智能体何时该接管控制流?一项合同抽取实验的对比
When should an agent own the control flow?
同一份合同字段抽取任务分别用固定流水线和自主选择步骤的智能体(基于 openai/gpt-oss-120b,temperature=0)各跑三遍,准确率基本持平:流水线每轮 80 个字段漏 1 个,智能体漏 2 个。
2026年10月7日 8分钟阅读
我一直在思考构建LLM系统时出现的一个设计问题:模型何时应该决定下一步做什么,何时该由代码控制流程?Anthropic关于构建有效代理的指南对预定义工作流和模型导向的代理做了类似的区分。我很好奇在一个具体例子中这种权衡会是什么样子,所以我做了一个小实验。
我用两种方式运行了同一个玩具合同提取任务。一种是固定流水线。另一种是代理,它使用搜索工具和计算器自行选择步骤。这个设置故意很小,所以请把数字当作示例,而不是基准测试。
准确率基本持平。在三次运行中,流水线每次运行80个字段中漏掉1个,代理漏掉2个。代理使用的token数量大约是5.6倍:平均每份合同1,981个,而流水线是356个。两种方法在运行之间都没有改变任何一个答案,但代理的成本确实变了。我们在一份合同上观察到的最大运行间差异是代理1,848个token,而流水线是7个。
所以这篇文章要讨论的问题是:如果步骤已经已知,那么让模型控制流程到底买到了什么?
把控制流放在知识所在的地方
我使用的经验法则是:通过问谁知道下一步以及何时知道,来决定谁拥有控制流。
- 如果在设计系统时操作顺序已知,就把它编码在代码中。提取四个固定字段就是这样。每份文档都经过相同的步骤,所以问模型下一步做什么只有一个正确答案,而你已经知道了。
- 如果下一步取决于你只在运行时才发现的信息,让模型控制可能就有意义。当第N+1步取决于第N步返回的内容时,代码无法在不枚举每个分支的情况下表达路径。
Anthropic的构建有效代理划了同样的界线。工作流通过预定义的代码路径编排LLM和工具。代理让模型指导自己的过程和工具使用。他们的建议是只在明显有帮助时才增加代理的复杂性。这个实验是给“明显”加上一个数字的小尝试。
设置
任务:从供应商合同中提取四个字段:双方、生效日期、以整美元计的年价值,以及以天计的终止通知期。
数据:为实验编写的20份短合同,分为五组。每组都有不同种类的混乱:
| 组 | n | 难点 |
|---|---|---|
clean | 5 | 没有;基线 |
date | 4 | 日期格式不同(07/04/2024、the 1st of August, 2023、2024.10.01) |
words | 3 | 金额用文字写出 |
math | 4 | 年价值没有说明,必须计算 |
buried | 4 | 正确的数字旁边有干扰项:滞纳金、保险限额、被取代的价格 |
控制项:
- 两种方法使用相同的模型(Groq上的
openai/gpt-oss-120b)、相同的字段规范、temperature=0和max_tokens=1024。 - 两者都由相同的精确匹配评分器评分,对大小写、
$和逗号进行轻度规范化。 - 每份合同在每种方法中运行三次。
流水线:一次调用使用固定模式,然后解析JSON。没有循环,没有工具。特别是没有计算器:它必须自己完成math组的算术。
resp = client.chat.completions.create(
model=MODEL, max_tokens=MAX_TOKENS, temperature=0,
messages=[{"role": "system", "content":
"Extract these fields from the contract and reply with ONLY a JSON object: " + FIELDS},
{"role": "user", "content": contract["text"]}])代理:一个标准的工具循环,最多8轮,有两个工具:
find_line(keyword)返回合同中包含该关键词的最多三个句子。calculate(expression)是一个精确算术求值器,只接受数字和+ - * /。
模型决定调用哪个工具、以什么顺序调用,以及何时停止并作答。
测量指标:
- 字段级准确率。
- API 报告的每份合同总 token 数:提示词加补全,对于 gpt-oss 还包括其推理 token。
- 答案或 token 数在不同运行之间是否发生变化。
结果
表 1:总体
| 流水线 | 智能体 | |
|---|---|---|
| 字段准确率(3 次运行的均值) | 99% | 98% |
| 各次运行的准确率范围 | 99–99% | 98–98% |
| 每份合同的模型调用次数 | 1.0 | 3.5 |
| 每份合同的 token 数(均值) | 356 | 1,981 |
| 智能体 ÷ 流水线 token 比 | 5.6× | |
| 答案在运行之间发生变化的合同数 | 0 / 20 | 0 / 20 |
| 单份合同跨运行的最大 token 波动 | 7 | 1,848 |
表 2:按合同组
| 组 | n | 流水线准确率 | 智能体准确率 | 流水线 token | 智能体 token | 比值 |
|---|---|---|---|---|---|---|
clean | 5 | 100% | 100% | 323 | 1,680 | 5.2× |
date | 4 | 94% | 94% | 356 | 1,764 | 5.0× |
words | 3 | 100% | 100% | 389 | 1,160 | 3.0× |
math | 4 | 100% | 100% | 346 | 3,039 | 8.8× |
buried | 4 | 100% | 94% | 382 | 2,130 | 5.6× |
token 列是每份合同的均值。比值列由它们计算得出。
所有错误都出现在 parties 字段,且每个错误在三次运行中均重复出现。合同 D1 被两种方法都漏掉了。B3 仅被智能体漏掉。评分器采用精确匹配,我保留的输出记录了漏掉哪个字段,但没有记录模型返回了什么。所以我无法告诉你这些是真正的误读,还是格式不匹配,比如“Weyland-Yutani”中连字符的变体。
math 和 buried 组被设计为智能体的主场,而它在那里并没有表现得更好。math 以 100% 打成平手,而 buried 正是智能体多出那一次漏掉的地方。
计算器的结果
在 math 组中,年度金额从未被写出来。有一份合同写着“每年 40,000 美元的基本费用,外加每月 2,000 美元的支持费”。答案是 64,000,必须有东西来计算它。
智能体有一个专为此构建的计算器。流水线不得不自己进行算术运算。两者都得了 100%。智能体在这一组上每份合同花费 3,039 个 token,而流水线为 346 个,约 8.8×。这是所有组中差距最大的。
计算器消除了算术,但没有消除围绕它的工作:
- 模型推理该做什么并发出工具调用。
- 循环追加调用的结果。
- 下一次请求会重新发送整个对话(系统提示词、合同、之前每一次调用和结果),然后模型才能继续。
流水线只发送一次合同。智能体每一轮都发送一段越来越长的对话。以下是循环,从 notebook 中略微精简:
for _ in range(max_turns):
resp = client.chat.completions.create(model=MODEL, messages=messages,
tools=TOOLS, max_tokens=MAX_TOKENS, temperature=0)
meter.add(resp) # each turn is billed for the full history
msg = resp.choices[0].message
if resp.choices[0].finish_reason != "tool_calls":
return parse_json(msg.content)
messages.append(msg)
for tc in msg.tool_calls:
messages.append({"role": "tool", "tool_call_id": tc.id,
"content": run_tool(tc)}) # history grows
return {} # out of turns counts as a miss我没有保存这些运行的逐轮轨迹,但循环表明成本是结构性的,而非偶然的。
只有当模型在没有工具时会把这一步做错,工具才能换来准确率。对这个模型来说,把 2,000 乘以 12 并不是难的部分,所以计算器只增加了成本,别无其他。换成更弱的模型,情况可能会变。如果真的变了,有一个更便宜的修复方法,把已知步骤留在代码里:让模型给出 {"amount": 2000, "per": "month"},然后你自己做乘法。
答案稳定,成本不稳定
对我来说,这是更有趣的结果。
在 temperature=0 处,两种方法在三次运行中都没有改变任何答案:两者均为 20 份合同中的 0 份。答案是稳定的。工作量则不是。流水线每份合同的最大波动为 7 个 token。智能体为 1,848 个,几乎等于其每份合同的平均成本。同一份合同、同一模型、相同设置和相同答案,在一次运行中比另一次多花了约 1,800 个 token。
在这个实现中,这来自于动态控制流。轮数在运行时决定,每增加一轮都会重新发送不断增长的历史记录,因此决策上的微小差异会变成令牌数量上的巨大差异。流水线成本取决于输入长度。代理成本还取决于模型在该次运行中碰巧做出的选择。
在操作层面,这体现在几个地方:
- 预算与预测。 每份文档的平均成本掩盖了分布。预测需要的是分布。
- 延迟与超时。 更多轮次意味着更多顺序往返。在典型运行中调整的超时或
max_turns上限会让长时间运行变成失败。 - 速率限制与容量。 每分钟令牌限制是被峰值触发的,而不是平均值。笔记本的运行说明特别警告 429 错误,正是因为代理对每份合同进行多次调用。
- 测试。 检查答案的评估对成本只字未提。回归测试也需要成本断言。
这些都不会让代理变得不可靠。它意味着在动态控制流下,资源消耗是与准确性并列的、需要单独预算、限制和监控的事项。
代理何时值得使用
这并不是在论证流水线优于代理。而是关于边界在哪里。当你无法可靠地提前写出路径时,代理就变得有吸引力:
- 开放式研究,下一步看什么取决于你刚刚发现了什么。
- 迭代调查,比如调试:读取错误,决定检查什么,重复。
- 可变工具使用,其中操作集合和顺序因输入而异,无法枚举。
- 工作流路径过多,固定流水线需要太多分支,以至于分支本身成为脆弱的部分。
即便如此,正确的形态通常是一个流水线,其中真正分支的部分由一个受限的代理步骤处理,而不是将代理包裹在所有内容之外。
局限性
- 任务很简单。 两种方法都接近准确率上限,这留给代理的提升空间很小。
- 合同是为实验编写的。 有 20 份短合同,没有附录、扫描文本或 40 页文档。更混乱的输入才是代理的搜索工具可能发挥作用的地方。
- 一个模型,三次运行。 这是在
temperature=0下的一个模型,没有显著性检验。不同的模型或提供商可能会改变准确率和比率。 - 令牌是代理指标。 没有测量美元成本或挂钟延迟。
- 评分很严格。 三个未命中(所有当事方)没有区分为误读与格式不匹配。
- 代理未优化。 更紧凑提示的代理可能会减少轮次。
最重要的是:这个实验并不表明流水线在一般情况下足够,或者代理没有用。 它衡量的是一个更窄的问题。当任务的控制流已经明确指定时,运行时自主性提供了多少额外价值?在这里,运行时自主性为我们带来了基本相同的答案,但令牌成本约为 5.6 倍,且成本分布宽得多。
复现实验
完整的笔记本包含一切:
- 20 份合同和黄金标签
- 两种实现和工具
- 评分器和令牌计量器
- 生成上述每个表格的单元格
它运行在 Groq 的免费层上:02-contract-extraction-pipeline-vs-agent.ipynb。你可以用不同的模型重新运行它,添加一份单次调用流水线处理错误的合同,或者只把 math 步骤移到代码中,看看它能否以流水线的成本达到 agent 的准确率。绝对 token 数会因模型和提供商而异。真正值得比较的是比例和分布范围。
来源:Hacker News · calm.rocks