我让 Agent 学会了“先看再说“——数据配方 + 观察事实的进阶之道
我让 Agent 学会了"先看再说"——数据配方 + 观察事实的进阶之道
以前是"我觉得",现在是"我看到"
一、故事得从上周说起
上周我加了 read_csv 和 read_excel,Agent 终于能"看懂"表格数据了。我挺高兴的,觉得数据分析这块应该差不多了。
然后我让它分析一个 POI 数据集。
我:“分析一下
上海_poi.xlsx,看看有多少列。”
Agent 说:“好的,我先读一下这个文件。” 然后调用了 read_excel。返回了预览信息:6 列,列名是 xxx、xxx……
一切正常。
然后我又问了一句,事情开始变得奇怪了:
我:“那你帮我统计一下,每个区有多少个 POI?”
Agent 说:“根据数据分析,黄浦区有 1243 个,徐汇区有 987 个……”
我:“等等,你什么时候算的?”
Agent:“我是根据文件内容推断的。”
我内心 OS:你连数据都没完整读取,你推断个啥?
它又开始编造了。
不同的是,它上次是"编造工具调用",这次是"编造数据统计"。它不是故意骗我——它就是觉得"看起来像这个数字"就说了。
但问题是,用户不关心"你觉得是多少",用户关心"实际是多少"。
于是,我意识到一个更深层的问题:
Agent 需要的不只是"能读取数据",它需要**“基于观察说话”**——它说的每一个数字,都必须有事实依据。
今天这篇文章,就讲我怎么解决这个问题的。
二、先解释几个概念
2.1 什么是"数据配方"?
概念解释:数据配方(Data Recipe)就是针对某类数据分析任务,提前写好的一套"标准作业流程"。
就像做菜有菜谱一样——你想做番茄炒蛋,不用从零开始研究先放番茄还是先放蛋,照着菜谱做就行了。
数据配方也一样:
| 配方 ID | 适用场景 | 标准步骤 |
|---|---|---|
export-file-list | 导出工作区文件列表 | 直接 create_excel,一步到位 |
profile-dataset | 预览/分析表格数据 | 先 profile_dataset 观察,再回答 |
filter-export | 筛选/清洗数据并导出 | 先 profile_dataset 观察,再 write_csv 导出 |
当用户说"把当前目录文件列表导出为 Excel",系统自动匹配 export-file-list 配方,告诉模型:“你应该直接用 create_excel,不要先 list_files 再自己拼。”
当用户说"分析一下这个 CSV",系统匹配 profile-dataset 配方,告诉模型:“你必须先调用 profile_dataset 观察数据,然后才能给出结论。”
配方的本质是:给模型一个"标准答案"级别的指引,让它少走弯路。
2.2 什么是"观察事实"?
概念解释:观察事实(Observed Fact)就是从工具执行结果中提取的可验证的信息。
工具执行完了,不是一扔就完了——系统会从中"提炼"出一些事实,记录下来:
| 工具 | 提取的观察事实 |
|---|---|
profile_dataset / read_csv / read_excel | dataset:data.csv:rows=2473、:columns=2 |
create_excel / write_csv | export:files.xlsx |
list_files | workspace:file_count=75 |
run_script | script:test.py:exit=0 |
这些观察事实存下来,用来做一件事:判断 Agent 说的话有没有依据。
2.3 什么是"观察拦截"?
概念解释:当 Agent 试图给出最终答案时,系统会检查——它说的那些数字,有没有对应的观察事实?
如果有,放行。如果没有,拦截。
比如 Agent 说"这个 CSV 有 2473 行",系统查了一下观察事实:
- 有
dataset:data.csv:rows=2473吗?有。✅ 放行。
如果 Agent 说"黄浦区有 1243 个 POI",系统查了一下:
- 有
dataset:data.csv:rows=1243?没有。❌ 拦截。
拦截后,系统会注入一条消息:
[harness] 你说的'黄浦区有 1243 个 POI'没有观察到相关事实。请先调用合适的工具获取数据,再给出结论。
然后强制进入下一轮。
三、具体新增了什么?
3.1 profile_dataset 工具
read_csv 和 read_excel 已经能做数据预览了,但它们有一个"副作用":它们既是"预览工具",也是"读取工具"。这就导致一个问题——当你只是想快速看一眼数据时,read_excel 会返回大量详细信息,可能超出你实际需要的范围。
所以我新增了一个专门的 profile_dataset 工具:
- 自动识别
.csv/.xlsx/.xls - 返回和
read_csv/read_excel完全相同的预览结构 - 支持
sample_offset(跳过前 N 行后再取样例)
它和 read_csv/read_excel 的区别在于:profile_dataset 是纯粹的观察工具,不承担"读取数据用于后续计算"的职责。它只回答一个问题:“这个文件长什么样?”
"观察"和"读取"分开,职责更清晰。
3.2 DataRecipes——匹配标准流程
DataRecipes 是一个规则匹配器,位于 src/policy/DataRecipes.ts。
当用户输入一个 prompt,它会按关键词匹配到最合适的配方,然后把配方的步骤注入到 ContextBuilder 的 system prompt 中。
比如用户说"把当前目录文件列表导出为 files.xlsx":
DataRecipes匹配到export-file-list- 注入 system prompt:“用户希望导出工作区文件列表。请直接使用
create_excel工具,参数from_workspace_files=true,路径为files.xlsx。不要先list_files再手动构造数据。” - 模型看到这个指引,直接调用
create_excel,一步完成。
用户少等好几轮,模型少绕弯路。
3.3 ObservedFacts——记录事实,防止编造
ObservedFacts 是一个事实收集器,位于 src/context/ObservedFacts.ts。
它做的事情:
- 收集:每次工具执行成功后,从
ToolResult中提取可验证的事实。 - 检查:当模型试图
final_answer时,检查回答中是否包含未观察的统计数字。 - 拦截:如果有,注入纠正消息,强制继续 loop。
它的判断逻辑很简单:
如果模型回答了 "共 1234 行"
但观察事实中没有 "dataset:*:rows=1234"
→ 拦截,要求调工具验证
效果:模型说的每一个数字,都必须有工具返回的证据支撑。
四、以前 vs 现在
场景:用户说"分析上海_poi.xlsx 有多少列"
之前:
Agent: 调用了 read_excel("上海_poi.xlsx")
read_excel: 返回预览,6 列
Agent: "有 6 列。"(正确)
用户: "那每个区有多少 POI?"
Agent: "我推断黄浦区 1243 个……"(编造)
现在:
Agent: 调用了 profile_dataset("上海_poi.xlsx")
profile_dataset: 返回预览,6 列
Agent: "有 6 列。"(正确,且有观察事实记录)
用户: "那每个区有多少 POI?"
Agent: "每个区……"(尝试 final_answer)
[ObservedFacts] 拦截!检测到未观察的统计数字。
[harness] "你说'黄浦区 1243 个'未观察到。请调工具获取实际数据。"
Agent: 调用了 run_script 或 SQL 工具……
Agent: "黄浦区实际有 1287 个 POI。"(现在有依据了)
多了一轮,但结论可信。
五、为什么这样设计?——三点思考
5.1 模型不该"估算"数据
模型在训练时见过大量数据分布,它确实能"猜"出一些统计数字大概在什么范围。
但问题是——用户要的是准确数据,不是大概范围。
“黄浦区大概有 1200 多个 POI"和"黄浦区有 1287 个 POI”——前者可能是正确的"估算",后者才是"事实"。但 Agent 把估算当事实说,这就不对了。
模型擅长推理,不擅长检索。让工具负责检索,让模型负责推理。
5.2 配方的本质是"经验复用"
写代码的时候,你不会每次都重新想"怎么读取 CSV",你会复用之前的代码。
Agent 也一样。DataRecipes 本质上是在复用最佳实践:
- 导出文件列表 → 用
create_excel(from_workspace_files=true) - 分析表格数据 → 先用
profile_dataset观察 - 筛选导出数据 → 先观察、再导出
这些"最佳实践"是开发者在调试中总结出来的,现在注入给模型,让它少踩坑。
5.3 观察事实是"理性对话"的基础
人和人之间的有效沟通,建立在"共识事实"之上。如果我说"外面下雨了",你说"没有啊",那我们就得先确认"雨"是什么、有没有真的在下。
Agent 和用户的沟通也一样。如果 Agent 说"有 1243 个",用户没法验证——他没有直接访问数据的权限。
ObservedFacts 的作用就是:让模型的每一个结论都有"证据链"可以追溯。 用户随时可以问:“你怎么知道的?” 系统会回答:“这是我从 profile_dataset 的返回里观察到的。”
六、和之前的功能怎么配合?
| 功能 | 和这次的关系 |
|---|---|
IntentPlanner | 拆子任务 → DataRecipes 提供"标准作业流程" |
read_csv / read_excel | 工具本身不变,但 profile_dataset 是更纯粹的"观察"版本 |
TaskStateTracker | 观察事实可以作为子任务"验证"的完成证据 |
FakeToolCallGuard | 防止编造工具调用;ObservedFacts 防止编造数据结论 |
TraceReport | 观察事实会记录在 trace 中,复盘时可以看到"结论是否基于观察" |
环环相扣,层层把关。
更多推荐



所有评论(0)