RAG Agent Benchmark 调优实战:从 85% 到 100%(答案 + 工具)
当你发现「答案对了,但工具选错了」,才算真正开始调优 RAG 系统。
前言:别急着调模型,先建 Benchmark
我们的系统是一个世界杯数据问答 Agent:FastAPI → Workflow Router → simple_qa / complex_flow / gossip,底层是 pgVector fact card + 分析视图(vw_*),上层是 LangChain Agent + 三级 Query Cache(L1 内存 / L2 Redis 精确 / L3 语义)。
功能跑通后,我们写了 20 条 Golden Case(球员统计、SQL 排行、语义检索、中国队等),用 benchmark/benchmark.py 对 /chat 做端到端评测。
第一轮冷跑(--skip-cache)结果:
85% 答案、65% 工具——说明不少题「路走歪了但 LLM 兜底对了」,也有 3 条是路由进了 Mock 流程,答案直接错。
下面先汇总我们实际用到、且可复现的提准方法,再按踩坑顺序展开。
方法速览:我们用到哪些手段
| # | 方法 | 作用层 | 解决什么问题 |
|---|---|---|---|
| 1 | Golden Benchmark + 自动化 runner | 评测 | 每次改 Prompt/路由可回归,避免「感觉变好了」 |
| 2 | --skip-cache 冷跑 | 评测 | 避免 query cache 命中掩盖真实准确率 |
| 3 | expected_answer_groups 同义词组 | Golden | 「13 / 十三」「孙雯 / Wen Sun」都算对 |
| 4 | validate_sql + expected_sql_contains | Golden | 不只判答案,还判 SQL 是否查了对的视图/条件 |
| 5 | prefers_simple_qa() 路由修正 | Router | 单实体统计题不再误进 complex_flow Mock |
| 6 | Prompt Step 1–4 工具决策树 | Agent | 球员 → SQL → 语义 → 搜人,顺序固定 |
| 7 | 工具 docstring 写边界 | Agent | @tool 描述适用场景,不只写「执行 SQL」 |
| 8 | player_aliases.json 中文俗称 | 数据/工具 | 大罗、贝利、C罗 → 精确 player_id |
| 9 | resolve_player_id 优先查 career card | 工具 | player_stats 先命中 fact card,再语义补充 |
| 10 | 分析视图 vw_* + Prompt Few-shot SQL | SQL | 不让 Agent 写不存在的 goals/players 表 |
| 11 | 禁止表名黑名单 + 结构化 SQL 错误 | SQL | 错表直接返回 {"error": ...},Agent 换工具或改 SQL |
| 12 | RESPONSE_RULES 禁止编造 | Agent | 无数据就说没有,不用「大概」凑数 |
| 13 | temperature=0 | LLM | 降低随机性,Benchmark 更可复现 |
| 14 | LangSmith trace(可选) | 观测 | 看每步 tool call,定位选错工具 |
| 15 | is_player_compare() 路由 | Router | 梅西 vs C罗 双人对比进 simple_qa |
| 16 | 「一共几次 + 年份」路由 | Router | 意大利冠军届次不再进 Mock |
| 17 | Prompt Step 0(评价/open 问法) | Agent | 「梅西表现怎么样」优先 semantic |
| 18 | expected_tools 多解 Golden | 评测 | SQL 或 semantic 都能答对的题允许多路径 |
尚未做、本文不当作已验证手段:Chunk 分块对比、Hybrid Search(tsvector + RRF)、SQL 自动 retry 循环、ROUGE/BERT 语义打分——可作为后续优化方向,但不写进本次 85%→100% 的路径。
问题一:路由误判——简单题进了 ComplexFlow Mock
现象
3 条失败 case 的共同特征:答案里是 【ComplexFlow · Mock 模式】,不是 Agent 正常输出。
| 问题 | 误触关键词 | Mock 里发生了什么 |
|---|---|---|
| 2022 决赛几个进球 | 多少个 | SQL 只 WHERE tournament_id='2022' LIMIT 10,没筛决赛 |
| 大罗生涯总进球 | 总共 | 走 semantic_search,没调 player_stats |
| 中国女足射手王 | 进球最多 | SQL 误用 competition='Men's' 男子榜 |
根因:COMPLEX_KEYWORDS 里「总共、多少个、进球最多」太宽,还没实现真正的 complex agent,Mock 流程却抢走了本该走 simple_qa 的题。
解决方案:prefers_simple_qa() 优先于 complex 关键词
在 workflows/route_keywords.py 里,对「单实体、可明确工具」的题强制走 simple_qa:
| |
效果:上述 3 条修复后,--skip-cache 冷跑 答案 20/20(100%);工具仍 15/20(75%)——见下文第二阶段。
问题二:Agent 工具选择——边界 + 决策顺序
现象
第一轮路由修复后,即使走对了 simple_qa,Benchmark 仍显示 工具正确率 75%(15/20)。典型分歧:
- 「梅西表现怎么样」→ golden 期望
semantic_search,Agent 因出现「梅西」走了player_stats(答案仍对) - 「梅西和 C罗谁进球更多」→ golden 期望
player_stats,路由进了complex_flow的sql_query(答案仍对)
说明:答案正确 ≠ 工具与 golden 一致,评测标准和 Prompt 优先级要分开看。
我们实际做的三件事
① 工具 docstring 写「干什么、不干什么」
| |
② System Prompt 用 Step 顺序,不是散落的「建议」
| |
③ 球员别名表,让 Step 1 真的能用中文问
etl/data/player_aliases.json + resolve_player_id():「大罗」→ P-62722(巴西 Ronaldo),再拉 worldcup-player_careers fact card。
问题三:SQL 生成——用视图,不用臆造表
现象
失败 case #2 若走 Mock,会生成:
| |
缺 M-2022-64 或 stage_name ILIKE '%final%',Benchmark 的 expected_sql_contains 判失败。
根因
Agent 若按「通用足球库」想象,会写 goals / players / matches——这些表在本项目不存在。
解决方案
① Prompt 只暴露 3 张分析视图 + 文档表,并给 Few-shot
| |
② 代码层拒绝非法表名,返回可读错误
| |
Prompt 约定:看到 error 就改 SQL 或换 player_stats / semantic_search,不要重复同一条错 SQL。
修复后 #2 实际生成:
| |
答案:6 个进球(常规时间 + 加时,不含点球大战)。
问题四:回答忠于工具数据
做法(已实现)
在 prompts.py 的 RESPONSE_RULES:
| |
配合 temperature=0,减少 Benchmark 上的数字漂移。
我们没有加单独的「验证节点」LangGraph step;若幻觉仍多,可在此基础上再加自查 prompt 或结构化输出校验。
问题五:Golden 与 Benchmark 脚本怎么设计
① 答案打分:关键词 + 分组 OR
| |
Golden 示例:
② SQL 打分:片段必须出现
③ 冷跑命令
| |
最终结果
调优分两轮,--skip-cache 冷跑,数据来自 benchmark/result.tool-opt.json。
第一轮:路由 + SQL Prompt(答案拉满)
路由修正 + Prompt/SQL 示例补全后:
- 答案 100%:20 条 golden 全部通过。
- 工具 75%:5 条「答案对但工具名与 golden 不一致」。
- sql_required_miss: 1:「意大利几次冠军」仍被 complex_flow Mock 抢走。
第二轮:路线 A + C 落地后(当前)
实现 is_player_compare、冠军届次路由、Prompt Step 0、expected_tools 后:
- 答案 100%、工具 20/20(100%) 同时达成。
- sql_required_miss: 0:意大利冠军届次走 simple_qa +
sql_query。 - sql_valid_rate 8/9:仍有 1 条 SQL 未含 golden 要求的片段(答案仍对);与「工具 100%」不矛盾。
| 阶段 | 答案 | 工具 | 延迟 | sql_required_miss |
|---|---|---|---|---|
| 基线 | 85% | 65% | ~5.1s | 1 |
| 第一轮(路由修复) | 100% | 75% | ~6.5s | 1 |
| 第二轮(A+C) | 100% | 100% | ~6.2s | 0 |
工具正确率:从 75% 到 100%(已实现)
答案到 100% 后,工具 75%(15/20) 说明 Benchmark 期望的「标准路径」和系统实际走的「有效路径」还没对齐。下面是当时 5 条 mismatch 及对应改法(均已落地)。
5 条 tool 不匹配一览
| # | 问题 | golden 期望 | 实际工具 | 答案 | 根因 |
|---|---|---|---|---|---|
| 4 | 梅西和 C 罗谁进球更多 | player_stats | complex_flow → sql_query | ✓ | 「和 + 谁更多」→ COMPLEX_WITH_AND,未进 prefers_simple_qa |
| 5 | 梅西世界杯表现怎么样 | semantic_search | player_stats | ✓ | Prompt Step 1:有球员名就优先 stats,与「评价/open 问法」冲突 |
| 11 | 2014 金手套得主 | player_stats | sql_query → semantic_search | ✓ | Agent 先写 SQL 查奖项,失败再语义;golden 假定直接 stats |
| 15 | 意大利夺得过冠军吗 | semantic_search | sql_query | ✓ | Agent 用 documents 查 Italy Winner,合理但 golden 标 semantic |
| 16 | 意大利几次冠军、哪些年 | sql_query | complex_flow → semantic_search | ✓ | 「一共几次」命中 COMPLEX_KEYWORDS,Mock 未生成 SQL |
这 5 条里 没有一条是「答错」,而是 评测口径 vs 工程取舍 的差异。
路线 A:改路由(已实现)
① 对比题进 simple_qa — is_player_compare():双方别名都能 resolve_player_id 时走 simple_qa(修 #4)。
② 「一共几次 + 年份」进 simple_qa — 在 prefers_simple_qa() 中:
| |
(修 #16)
路线 B:微调 Prompt(已实现)
Step 0 — 「怎么样 / 表现 / 评价」优先 semantic_search,即使用户提到球员名(修 #5)。
奖项题 — 金手套/金球得主优先 semantic_search 或 player_stats,不强行写复杂 SQL。
路线 C:Golden 多解(已实现)
benchmark.py 增加 match_expected_tool(),支持 expected_tools 列表(任一命中即 pass):
| |
Golden 示例(修 #11、#15):
实测:A + B + C 落地后,--skip-cache 冷跑 tool 20/20(100%),答案仍 100%。
复现命令:
| |
总结
- Benchmark 先行——20 case + runner,改一行就跑一遍。
- 先看路由,再看 Prompt——Mock workflow 误判的代价远大于「模型不够聪明」。
- SQL 用视图 + Few-shot + 黑名单——比塞 9 张原始表结构更有效。
- 中文 RAG 要做别名——否则
player_stats在中文问题上不稳定。 - 分开看 answer 与 tool,再对齐口径——本轮最终 100% 答案 + 100% 工具;多路径题用
expected_tools比死磕单一工具名更贴近工程现实。
调优不是一次性工作;每次加 workflow(session memory、complex agent 真实现)都要重新跑 --skip-cache,避免 cache 和 Mock 路径把指标「刷好看」。
附录:按改动文件对照
| 文件 | 提准相关改动 |
|---|---|
benchmark/golden.json | 20 case、同义词组、expected_tools |
benchmark/benchmark.py | match_expected_tool、validate_sql、--skip-cache |
workflows/route_keywords.py | prefers_simple_qa()、is_player_compare() |
workflows/router.py | simple_qa 优先于 complex 关键词 |
prompts.py | Step 0–4、SQL 视图示例、禁止编造 |
workflows/simple_qa.py | 四工具 docstring、LangChain agent |
tools.py | resolve_player_id、execute_sql 黑名单与错误结构 |
etl/data/player_aliases.json | 中文俗称 → player_id |