简介《2024 ChatBI与智能体实战手册八大案例共一百三十四页》是一份面向数据分析、大模型应用和商业智能从业者的实战资料汇集平安人寿、滴滴、喜马拉雅、腾讯、快手、阿里巴巴、网易等多个企业团队的落地实践覆盖智能问数、对话式取数、自动化报表、结构化查询生成、分析流程再造等环节帮助读者理解如何借助大模型将传统商业智能升级为更智能、更自动、更实时的分析系统。资源为单个PDF文件共134页压缩包大小约9.33MB适合数据分析师、算法工程师、技术管理者及企业决策人员按章节研读也可作为智能BI方案选型与架构设计的参照。目前该手册已有433人学习或下载。内容详细记录了各家企业在项目背景、总体架构、产品效果、落地挑战等方面的真实经验包括数据指标治理、模型调优、跨部门协作、权限与安全控制等细节并给出具体应对思路与操作建议有助于读者规避常见问题并快速构建可落地的智能分析与智能体应用。1. ChatBI Agent为什么对话式BI必须有一个“会办事”的大脑2024 年做 ChatBI 的人都会遇到同一个场面内部工具上线第一周产品经理围着问“上个月华东区退货率多少”大模型答得又稳又快第二周业务改口问“那剔除促销的呢”“按周看呢”模型开始一本正经地编数字。ChatBI 的本质是把自然语言变成可执行的查询而 Agent 的出现是为了让这套查询在真实数据环境里“有流程、有记忆、有边界”。这不是一个 Demo 项目而是数据平台团队把大模型接进数仓后的第一次认真整合。适合谁读正在搭内部数据助手、想把对话查数做进 BI 产品或者刚拿到一份 ChatBIAgent 实战手册、想提炼出自己团队可落地方案的从业者。你不需要会训练模型但需要理解 SQL、数据字典和执行权限这三件事如何在一个 Agent 工作流里协作。2. 先拆架构再写代码ChatBI 的 Agent 分层与框架选型2.1 为什么纯 NL2SQL 撑不起 ChatBI很多团队的第一版 ChatBI 就是“Prompt 大模型 数据库连接串”。单表问“销售额多少”没问题一旦进入真实业务问题就出在三个地方。第一用户不会给你一个结构完整的查询而是“那湖北呢”“按月份呢”这是多轮指代第二一个指标在企业里有固定的口径“销售额”到底是订单金额还是实收金额模型不知道第三SQL 生成错了你需要能看见是哪一步错的而不是对着黑匣子干瞪眼。Agent 化就是把这三点变成显式的流程对话状态、指标查询、执行反馈各司其职。我一般把 ChatBI 的 Agent 定义为“一个会调工具、会看执行结果、会决定要不要重来一遍的调度器”。它不负责“聪明”负责的是“稳定”——让模型的每一次发挥都被限定在可控的路径里。这个过程在 Agent 开发里叫编排也是 2024 年 ChatBI 和早期 NL2SQL 工具最本质的区别。2.2 一套可落地的六层架构与每层职责ChatBIAgent 的架构远不止“模型 数据库”。实际落地上我会拆成六层每一层解决一类问题层级主要职责关键产物/组件交互层处理用户多轮输入、前端图表联动对话管理、消息协议Agent 编排层规划调用顺序、维护状态、重试状态机 / LangGraph / 自研编排语义层提供指标口径、维度字典指标字典、语义模型解析执行层生成 SQL、执行、流式返回NL2SQL、SQL 审核器安全控制层权限过滤、数据脱敏表级 / 行级权限策略可观测层记录 Agent 每步决策Trace、执行日志、评估集这六层里最容易忽略的是语义层和可观测层。语义层决定了“同一个指标谁都算得一样”可观测层决定了你能不能给团队讲清楚“这次为什么答错了”。如果你们数据仓库已经有现成的指标平台语义层可以直接接它的口径接口不要自己再造一套。执行时的数据流通常是这样的用户输入先进交互层编排层决定先调用语义层确认指标口径再交给解析执行层生成 SQL执行前过安全控制层执行后的结果和中间决策全部写入可观测层。这条链路里任何一个环节缺失都会在特定场景下暴露问题。比如漏掉语义层指标口径就会乱漏掉安全层越权查询就会在某个 join 场景下钻出来。2.3 Agent 框架怎么选LangGraph、Dify 还是自研做 ChatBI 的 Agent常见的路线有三条LangGraph、Dify、自研编排。我的选型判断是验证期可以用 Dify 这类低代码平台最快看到效果生产期只要涉及权限细粒度控制、SQL 执行卡点就值得转到 LangGraph 或自研。这里的判断依据是 ChatBI 的特殊性——它不是普通的问答 Agent它的“工具”是数据库执行环境工具调用的失败率天然比搜索、计算器高需要精细的恢复逻辑。比如 LangGraph 的优势是可编排的图结构先规划再执行执行失败能回到“修正”节点而不是整体结束。自研编排的好处是完全可控比如在 run_sql 之后插入一个独立的“数据脱敏节点”这类节点在通用 Agent 框架里不一定有现成的。框架选型还要考虑团队语言栈Python 团队选 LangGraph 的学习成本比较低如果技术栈是 Java自研一个轻量状态机反而比维护一套 Python 服务更省事。当出现下面几种信号时说明当前框架该换了想在某一步插入自定义钩子但平台不给开放接口工具调用参数无法完整记录排障靠猜多轮对话状态经常错乱同一句话在两个小时前和现在问结果不一样。这些信号一旦出现两个就不要再“调配置硬扛”。2.4 从 134 页案例里提炼共性先分类再抄作业看实战手册有个技巧不要照着某个案例的页面抄配置而是先把案例按“行业 问题类型”做分类。像这本手册标题提到的八大案例我建议拿到手先做一件事——不要按顺序读而是把八个案例按“业务动作”打标。常见的是这八类经营驾驶舱问答、供应链异常分析、销售归因、用户行为分析、财务指标核对、库存预测、客服工单分析、运营活动复盘。这八类共享的能力是一套指标字典、一套可复用的 NL2SQL 解析链路、一套相同的权限模型。也就是说做完第一个案例剩下七个案例的侧重点大多在指标口径差异上而不需要重新搭 Agent 骨架。真正决定项目周期长短的不是模型选哪个而是你们的指标字典建得怎么样、Schema Linking 做不做、执行权限谁来管。这三件事在任何案例里都是底座。3. NL2SQL 还是核心从 Schema 裁剪到 SQL 生成的完整链路3.1 Schema Linking几百张表怎么裁剪到“当前问题”需要的几张ChatBI 翻车最多的一步不是模型不够聪明而是把整个数仓几百张表的 schema 全部塞给模型。我见过最典型的现象表一多模型开始“幻觉”把订单表的字段 join 到用户表的错误字段上。解决它靠两个手段关键词召回 向量召回先缩小候选表集合再注入到 Prompt。用一个函数表达这个思路import re def link_schema(query: str, table_catalog: list[dict]) - list[dict]: 从 table_catalog 中召回与 query 最相关的若干张表 table_catalog: [{table_name: ..., columns: [...], description: ..., sample_rows: {...}}, ...] 返回: 裁剪后的表集合每张表附带相关列和样本值 # 1. 先按关键词命中做粗筛表名、列名、注释里有 query 中的词直接命中 hit_by_keyword [] for table in table_catalog: haystack .join( [table[table_name]] [col[name] col.get(comment, ) for col in table[columns]] [table.get(description, )] ) # 用简单分词提取 query 里的候选词中文按字符片段、英文按小写单词 tokens re.findall(r[a-zA-Z_], query.lower()) if any(t in haystack.lower() for t in tokens) or query in table.get(description, ): hit_by_keyword.append(table) # 2. 如果粗筛结果过多就按“表注释包含业务域 列数最少”优先排序 if len(hit_by_keyword) 5: hit_by_keyword.sort(keylambda t: (len(t[columns]), -len(t.get(description, )))) # 3. 兜底一个词都没命中时回退到最近常用表业务侧配置 if not hit_by_keyword: hit_by_keyword [t for t in table_catalog if t.get(is_hot, False)][:3] return hit_by_keyword[:4]逻辑说明这个函数做的是“先粗筛、后截断”的工程兜底不是严格的机器学习排序。真实生产里关键词召回结果往往噪声很大所以我会叠加一层向量召回——把表描述和列注释做 embedding 后用查询向量计算相似度合并两种结果。参数说明粗筛结果超过 5 张就按“列数少优先”原因是大表注入会让模型注意力分散兜底热表is_hot是人工在后台配置的常用表这个配置项在冷启动阶段比模型更可靠。3.2 让模型“见过”数据样本值与数据字典的注入模型不知道你的 status 字段里存的是 PAID 还是 1这是所有 NL2SQL 应用都绕不开的坑。所以注入表结构时一定要带上样本值。常见做法是每个字段附 2~3 个 distinct 样本值再额外带上一份数据字典说明。这个步骤很便宜但对准确率提升非常明显尤其是状态类、类型类字段。比如下面这段注入格式表: order_info 说明: 订单主表一单一行 列: order_id | bigint | 订单ID主键 user_id | bigint | 用户ID关联 user_info.user_id status | string | 订单状态样本值: PAID, CLOSED, REFUNDED amount | decimal(18,2) | 实付金额单位元 pay_time | timestamp | 支付完成时间样本值: 2024-06-01 12:33:10注意 status 这类字段只给样本值还不够最好把业务侧的状态流转规则用一句话写进表注释里否则模型很可能把“已支付”映射到一个不存在的枚举值上。数据字典建议做成独立表每天从数仓元数据同步一次Agent 启动时加载到内存里。这里顺便说一个参数每次注入的样本值不要超过 3 个超过之后模型反而会被低频值带偏。3.3 一份够用的 Prompt 模板与参数设定ChatBI 的 Prompt 不是越长越好但顺序要稳定系统身份 - 方言约定 - 表结构 - few-shot - 用户问题 - 多轮摘要。我用 Python 拼一个最小可用模板SYSTEM_TEMPLATE 你是一个严谨的数据库查询工程师负责将用户问题转换为符合 {dialect} 语法的 SQL。 规则 1. 只使用提供的表结构中的表和列禁止臆造字段。 2. 金额字段一律按 decimal 处理禁止用整型比较。 3. 涉及时间条件时优先使用 {time_column} 字段。 4. 若用户问题涉及指标口径必须使用指标字典中的定义。 5. 只返回 SQL不要额外解释。 表结构 {schema_text} 历史对话摘要 {conversation_summary} 指标字典节选 {semantic_dict} def build_messages(query: str, schema_text: str, dialogue_summary: str, semantic_dict: str, dialect: str mysql): system_prompt SYSTEM_TEMPLATE.format( dialectdialect, time_columnpay_time, schema_textschema_text, conversation_summarydialogue_summary, semantic_dictsemantic_dict, ) return [ {role: system, content: system_prompt}, {role: user, content: query}, ] # 实际调用时参数示例temperature0, top_p0.9, max_tokens2048逻辑说明把历史对话摘要放在模板中间是为了让模型在看到用户问题之前先建立上下文指标字典单独成段是为了和表结构区分开降低模型把“用户说的销售额”直接映射到字段名的概率。参数说明temperature 必须设成 0 或接近 0否则相同的问题会生成不同的 SQL业务侧无法接受max_tokens 设 2048 不是“够用就行”SQL 会带着注释、多表 join 很长截断是最难排查的报错之一。top_p 我通常保留 0.9因为纯贪心采样有时会让模型在边界问题上过于自信。3.4 用工具调用约束输出而不是靠“只返回 SQL”让大模型输出 SQL 再正则解析是最容易翻车的方案。2024 年主流大模型普遍支持函数调用/工具调用模式ChatBI 应该把 run_sql 声明为一个工具让模型“填写参数”而不是“输出代码”。这样同一个模型输出格式被框架约束住可解析率会明显提升。工具定义大致长这样{ name: run_sql, description: 在只读数据源上执行一条 SELECT 语句并返回结果集, parameters: { type: object, properties: { sql: {type: string, description: 合法的 SELECT 语句} }, required: [sql] } }这种方式的收益有两个一是模型不再纠结“要不要带分号”“要不要加 markdown”它只需要填一个字符串参数二是你的代码可以统一从 arguments 字段拿到 SQL再做 SQL 审计、权限过滤整条链路的可观测性更好。要留意的是工具调用模式下的问题描述要写在工具 description 里写清楚“只读”“SELECT 语句”这些边界模型会更老实。3.5 SQL 失败后的自动纠正让错误信息成为第二次机会第一次生成的 SQL 大概率有语法错误或字段不存在。不要直接把这个错误抛给用户。我一般在执行层捕获错误后把数据库原样错误信息拼一个“修正”Prompt让模型带着错误信息重新生成一次。常见做法是给两次机会第一次直接生成第二次带错误第三次再失败就转人工兜底话术。def generate_sql_with_retry(question: str, schema_text: str, max_retry: int 2): last_error None for attempt in range(max_retry 1): messages build_messages(question, schema_text, dialogue_summary) if attempt 0: messages.append({ role: user, content: f你上一次生成的 SQL 执行失败错误信息如下请修正后重新生成\n{last_error} }) sql call_llm(messages) # 实际接入你自己的模型服务 ok, last_error dry_run_sql(sql) # 在只读连接上 EXPLAIN 或 LIMIT 0 if ok: return sql raise RuntimeError(fSQL 生成失败最后错误{last_error})逻辑说明这里的关键是用 dry_run 而不是直接执行完整 SQL也就是在只读连接上对目标 SQL 做 EXPLAIN 或加 LIMIT 0这样既能拿到语法/权限错误又不会真的查询全量数据。参数说明max_retry 设 2 是性价比平衡点试第三次成功率不高反而让用户等太久实际生产里还会在循环外面加一个总体超时比如 30 秒内必须返回避免大模型响应慢导致用户长时间等待。4. Agent 把查数变成工作流编排、记忆与执行边界4.1 从“一问一答”到“计划-执行-反馈”的工作流当 ChatBI 需要处理多轮对话、多个数据源、不同指标的联动时用一段代码串行调用已经不够了。Agent 化的核心是把查数过程定义成一个状态机接受用户问题、规划需要的工具、逐个执行、观察结果、决定是继续还是结束。这正好是 LangGraph 这类 Agent 框架擅长的事情。下面给一个最小可运行的 ChatBI Agent 状态图from typing import TypedDict, Literal from langgraph.graph import StateGraph, END class ChatBIState(TypedDict): question: str sql: str result: str retry_count: int def planner(state: ChatBIState): # 规划节点根据问题判断是否需要查数还是直接回答问题 return {sql: generate_sql_with_retry(state[question], schema_text)} def run_sql_node(state: ChatBIState): # 执行节点在这里会经过 SQL 审计和权限过滤 result execute_readonly_sql(state[sql]) return {result: result} def guardrails(state: ChatBIState): # 安全节点检查结果行数、敏感字段决定是否放行 if 敏感字段 in state[result]: return {result: 该结果包含敏感信息已拦截} return {} def answer_node(state: ChatBIState): # 回答节点把结果翻译成自然语言必要时生成图表配置 answer explain_result(state[question], state[sql], state[result]) return {answer: answer} def should_retry(state: ChatBIState) - Literal[retry, answer]: # 条件边SQL 结果为空或执行失败时回到修正节点 return retry if state[retry_count] 2 else answer graph StateGraph(ChatBIState) graph.add_node(planner, planner) graph.add_node(run_sql, run_sql_node) graph.add_node(guardrails, guardrails) graph.add_node(answer, answer_node) graph.set_entry_point(planner) graph.add_edge(planner, run_sql) graph.add_edge(run_sql, guardrails) graph.add_edge(guardrails, answer) graph.add_conditional_edges(answer, should_retry) graph.add_edge(answer, END)逻辑说明这个图表达的是 ChatBI 最核心的流转路径。planner 节点只负责生成 SQLrun_sql_node 执行时走只读连接guardrails 检查结果是否包含敏感字段answer_node 最后把结果翻译回人话。参数说明条件边里 retry_count 上限为 2指的是整体重试链路而不是单个 SQL 的修正次数实际使用时还需要额外设置超时节点比如 30 秒无响应就终止防止某个节点卡死导致整个会话挂起。4.2 工具注册的模式Agent 怎么知道能用什么在 Agent 开发里工具Tool是“Agent 能调用的世界”。对 ChatBI 来说常见的工具集可以收敛为五类list_tables列出用户有权限的表、get_schema获取表结构、run_sql执行查询、get_chart_config生成图表配置、get_semantic_metric获取指标口径。把这五个工具注册给模型比暴露几十个 API 要稳定得多。工具越多模型选错的概率越大。一个工具注册的实质是一个 JSON Schema 描述比如 get_semantic_metric 的定义{ name: get_semantic_metric, description: 根据指标中文名或英文名获取该指标在指标平台上的标准口径包括计算公式与适用维度。, parameters: { type: object, properties: { metric_name: { type: string, description: 指标中文名例如销售额、退货率、客单价 } }, required: [metric_name] } }注册工具时要注意一个细节description 里要写清楚工具能力边界比如 get_schema 的描述要补充“只返回列名、类型、注释和样本值不返回数据”。模型会根据描述判断该用哪个工具。我们在生产里见过最多的问题是模型把 get_chart_config 当成查数工具来调用最后拿回来一张空图表配置。原因是 description 不够明确没写“本工具不访问数据”。4.3 多 Agent 协作与记忆的取舍2024 年 Agent 项目落地有个趋势一上来就设计四个角色互相协作。但 ChatBI 的实践告诉我角色不是越多越好。常见做法是两个角色就够用一个查数 Agent负责 SQL 生成、执行与校验一个解释 Agent负责任务拆解、指代消解、结果转述。如果再加一个“指标 Agent”专门查指标字典也可以接受但对大多数团队来说两个角色的通信成本已经能覆盖八类业务场景。记忆这块ChatBI 需要的是两层记忆。短期记忆指当前会话的若干轮对话尤其是“上个月”“那华北呢”“按季度”这些指代依赖它在回答前先做上下文摘要。长期记忆指用户或团队常用的指标、表、过滤条件比如“运营团队总爱把活跃用户定义成登录次数大于 1 的用户”这一类要落到业务配置里而不是让模型自由发挥。实现上短期记忆可以用滑动窗口 摘要压缩长期记忆就是一张配置表Agent 每次启动时加载。多 Agent 协作时还要约定通信协议查数 Agent 返回的结构里必须包含 sql、execution_time、error_message 三个字段解释 Agent 只能消费这三个字段不允许直接读数据库。这个约定能避免“Agent 之间互相猜”的情况。4.4 权限沙箱是硬控制不能只靠提示词ChatBI 的权限不同于普通问答SQL 一旦生成就具备访问任意表的能力。因此要在执行层做表级和行级权限的强制校验而不是把“只能访问授权表”写进 Prompt。一个最小的权限控制函数是这样def enforce_permission(user_roles: set[str], sql: str, table_acl: dict[str, set[str]]) - str: 表级权限控制解析 SQL 涉及的表与用户的角色权限比对 table_acl: {table_name: {role1, role2, ...}} 返回: 通过校验的 SQL不通过则抛出 PermissionError # 用 sqlglot 解析 SQL 中的表名避免正则误判 from sqlglot import parse_one tables {t.name for t in parse_one(sql).find_all(Table)} allowed_tables set() for table, roles in table_acl.items(): if user_roles roles: allowed_tables.add(table) denied tables - allowed_tables if denied: raise PermissionError(f用户无权访问表: {denied}) # 行级控制如果用户有部门维度限制自动拼接 WHERE 条件 if row_filter in user_roles: sql fSELECT * FROM ({sql}) AS filtered WHERE dept_id IN (SELECT dept_id FROM user_dept WHERE user_id CURRENT_USER) return sql注意行级过滤用子查询拼接的方式适合轻量场景生产环境更推荐把权限下推到数仓视图层否则 join 场景下容易被穿透。逻辑说明这里用 sqlglot 做 SQL 解析而不是字符串匹配是因为用户生成的 SQL 里可能有子查询、CTE正则很难覆盖。参数说明table_acl 的键是数据仓库里的物理表名值是允许访问该表的角色集合行级过滤示例用的是子查询拼接这种方式在生产中要谨慎最好在 SQL 生成阶段就把行级权限作为系统指令注入执行层只做复核。5. 八大案例背后的共性坑五条高频踩坑与排查记录拿到一份 ChatBIAgent 案例手册你会发现案例之间看似行业不同翻车点却高度相似。而且这些坑在手册里往往一笔带过因为作者默认读者已经有生产环境经验。这里整理五条我们在一线落地时遇到的高频问题每条按“现象 - 原因 - 解决”展开。这五条不是算法玄学而是工程债先还债再谈调参。5.1 坑一库变大之后准确率断崖下跌现象Demo 阶段就三张表什么问题都答得对。接进真实数仓几百张表、字段上万以后SQL 生成准确率直接掉二三十个百分点还出现了把订单表 join 到商品画像表这种低级错误。原因整个 schema 全部注入 Prompt模型注意力被无关表稀释同时字段名相近的表order_info、order_detail、order_refund会互相干扰模型分不清该用哪一张。解决第一步把 Schema Linking 从“全量注入”改成“召回收敛”只注入与问题相关的 2~4 张表第二步给每张表加一段人工写的 description告诉模型这张表“管什么”第三步把表名冲突的公共字段在系统 Prompt 里显式标注各自的物理含义。我们实测过单是做完整表描述准确率就能回升 10 个百分点以上。5.2 坑二模型在“自创指标”算得可快了可口径全错现象用户问“销售额”模型直接 SELECT SUM(amount) FROM order_info但业务里的“销售额”要排除退款单、要按支付成功时间算而且和指标平台对不上。业务一看数字不对立刻不信任整个系统。原因模型没有访问指标字典只能按字段名“望文生义”。这是 ChatBI 特有的问题——不是 SQL 语法错而是业务口径错语法越对错得越隐蔽。解决强制让 Agent 在生成 SQL 之前先查指标字典get_semantic_metric如果指标字典里存在该指标就必须用字典里的口径。注意“强制”两个字不是靠 Prompt 提示而是在编排层加一个条件节点planner 的输出里必须带 metric_id否则不走 run_sql 节点。这个卡点救了很多团队的口径一致性。5.3 坑三“那华北呢”“按季度看呢”这种省略句没人接得住现象第一句问“华东区销售额”第二句问“那华北呢”Agent 生成了“SELECT 销售额 FROM ...”却没带任何区域条件或者带上了一个不存在的区域枚举值。原因多轮指代没有做归一化。模型拿到的是单轮 query没有把上一轮的条件合并进来。解决在进入 Agent 工作流之前加一个“指代解析 条件继承”节点。用大模型把历史对话和当前轮问题合成一个完整问题再交给 planner。同时把“区域”“时间”“维度”这三类最常被省略的条件在合成节点里显式要求补全。生产里的做法是这一轮节点单独记录一条 trace方便排查“哪一轮把条件丢了”。5.4 坑四SQL 报错后对话直接“死”掉用户以为系统崩了现象Agent 生成 SQL 执行报错直接返回“查询失败”用户追问什么都是同一句话或者整个会话状态被重置。原因没有把执行错误带回给生成端做修正也没有定义“失败之后的兜底路径”。模型把用户的一句追问当成一次全新会话处理前面所有的上下文全部丢失。解决按上面第 3.5 节的思路加自动修正链路并限定两次重试第二次重试仍失败时走一个“转人工”兜底节点把错误信息、用户问题、生成的 SQL 拼成一条记录给到数据团队。这个兜底节点在内部工具里很有用它既给了用户出口也给团队留下了可排查的线索。5.5 坑五测试账号查不到数据线上账号却能看到越权内容现象权限测试时发现只用行级权限过滤的 SQL在某些 join 场景下能绕过限制——比如用户只能看华东但 join 了一张大区表之后把华北数据带出来了。更隐蔽的是测试时因为数据权限太严什么都查不到为了演示把权限放开忘记收回。原因行级权限拼在 SQL 的最外层被 join 和子查询穿透权限放开是人工配置没有随环境发布。解决权限过滤要落到“数据源层”而不是 SQL 字符串层。能做两件事一是把行级权限下推到数仓的视图层不同角色映射不同视图Agent 只能访问视图二是权限配置纳入代码仓库走 review 流程测试、预发、生产三套环境各自独立。视图下推之后副作用是表名会变多需要同步更新 Schema Linking 的目录。快速定位问题时可以参考下面这张表问题表现优先排查层检查项几乎所有问题都答不对解析执行层Schema 注入是否裁剪、样本值是否缺失答得快但口径不对语义层指标字典是否接入、Agent 是否强制查口径第二轮开始乱交互层指代消解、上下文摘要是否做了轮次合并权限相关投诉安全控制层视图下推、角色映射、环境配置6. 从 134 页手册落到可汇报的数字给 ChatBI 建一个最小 Evals 集手册读得再透也不如一个能重复执行的评测集有说服力。ChatBI 的评测不是看几个 Demo 问题答得漂不漂亮而是要用一组固定的业务题在每次改 Prompt、换模型、调工具之后重跑一遍用数字判断改动到底对不对。这是 Agent 开发里最容易被跳过、也最值得先做的一步。6.1 建题50 条问题覆盖四类难度建议从真实用户提问里挑 50 条而不是凭空编。分布可以这样控制单表简单查询 15 条、多表 join 15 条、指标口径计算 10 条、多轮指代 10 条。每条题除了问题本身还要记录标准 SQL 或标准答案。构造方式很朴素让数据团队从过去三个月的工单、邮件、即时通讯里收集用户问过的问题去重后挑出高频的 50 条。6.2 跑分一个能用的最小评测脚本下面是一个评测集执行脚本的骨架核心是“每条题跑一遍 Agent然后对比 SQL 执行能力和答案一致性”import json def run_evals(cases: list[dict]) - dict: cases: [{id: 1, question: 华东区上个月销售额, gold_sql: ..., category: multi_join}, ...] 返回整体通过率 分类准确率 stats {total: 0, executable: 0, matched: 0, by_category: {}} for case in cases: stats[total] 1 result run_agent(case[question]) # 调用你的 ChatBI Agent 接口 # 指标1SQL 可执行 if result.get(sql) and dry_run_sql(result[sql]): stats[executable] 1 # 指标2结果一致——对只读连接执行 gold_sql 和生成的 SQL比较结果集 gold_result execute_readonly_sql(case[gold_sql]) agent_result execute_readonly_sql(result[sql]) if result.get(sql) else None if gold_result agent_result: stats[matched] 1 # 按类别汇总 cat case[category] stats[by_category].setdefault(cat, {total: 0, matched: 0}) stats[by_category][cat][total] 1 stats[by_category][cat][matched] (gold_result agent_result) return stats逻辑说明这个脚本对比的是结果集而不是 SQL 文本——同一句业务问题可能有两种完全不同的写法但结果应该一致。参数说明dry_run_sql 在前面定义过用于检查可执行性结果一致性比较前通常还需要对结果排序后做 hash因为数据库返回顺序不稳定。跑完以后报告里写“整体结果一致率 76%多表 join 类 63%”这种数字比写“效果不错”有用得多。6.3 把评测结果变成发布门禁我现在的工作习惯是任何一个 Prompt 改动、工具参数调整、模型版本升级都必须先在测试集上跑一遍对比基线之后才能发布。哪怕只提升一两个百分点也说明改动方向是对的如果多轮指代准确率掉了就说明新的改动动了不该动的地方。这个习惯帮我们避免了很多次“凭感觉优化、上线翻车”的尴尬。希望这一套方法论能帮到你少走一些我们走过的弯路。本文还有配套的精品资源点击获取
