语言模型不擅长算术,但它擅长写会算术的程序
语言模型会犯算术错误,这不是秘密。你问 GPT-4 "1234 × 5678 等于多少",它可能直接给你一个错误答案,语气还很确定。但如果你让它写一段 Python 代码来计算,然后执行这段代码——答案就对了。
这个观察是 Program of Thoughts 和 Chain of Code 的起点,也是整个"代码作为推理接口"这条研究路线的起点。理解这两篇论文,就理解了为什么大量现代 Agent 系统选择"生成代码然后执行"而不是"纯语言推理"。
这是"Code as Agent Harness"系列的第一篇,从机制层面讲清楚代码是怎么成为 Agent 推理的底层载体的。
Chain of Thought 的根本局限
先理解 Chain of Thought(CoT)在哪里失效。CoT 让模型在给出答案前先写出推理步骤:
问题:一家商店原价 120 元,打八折后,再减 10 元,最终价格是多少?
思考:120 × 0.8 = 96,96 - 10 = 86
答案:86 元
CoT 在需要多步推理的任务上比直接回答效果好得多,原因很清楚:把隐式的推理链显式化,模型的每一步都有上下文支撑。
但 CoT 有一个本质问题:推理和计算混在同一个"流"里。当计算很复杂时——大数乘法、多步累积误差、统计计算——语言模型的 token 预测能力并不擅长精确执行这些操作,错误就会产生并累积。
CoT 里的"计算"实际上是语言模型在模拟计算,而不是真的执行计算。这个差别很关键。
Program of Thoughts:把计算外包给解释器
PoT(2022,发表于 TMLR 2023)的核心思想只有一句话:让语言模型只负责推理逻辑,把计算外包给 Python 解释器。
同样的问题,PoT 生成的不是自然语言推理,而是一段程序:
# PoT 生成的代码
original_price = 120
discount_rate = 0.8
additional_discount = 10
discounted_price = original_price * discount_rate
final_price = discounted_price - additional_discount
print(final_price) # 86.0
然后这段代码真的被执行,得到数值结果。答案来自解释器,不来自语言模型的"计算"。
这个分工为什么更好?
可靠的计算。Python 的乘法和减法不会出错。你不需要信任语言模型算 120 × 0.8,只需要信任它能把问题翻译成正确的代码结构。
可验证的中间步骤。代码是可读的、可检查的。如果结果错了,你可以看代码找逻辑错误,而不是在自然语言里猜模型哪步推理出了问题。
组合性。变量名和函数调用让计算步骤之间的依赖关系显式化。final_price = discounted_price - additional_discount 比"96 减去 10"更清楚地表达了计算结构。
PoT 的实验结果:在 GSM8K、AQuA、FinQA 等数学推理 benchmark 上,PoT 比 CoT 平均提升约 12%。提升在需要多步数值计算的任务上尤其明显——这正是 CoT 最容易出错的地方。
Chain of Code:LMulator——用语言模型模拟解释器
PoT 解决了数值计算问题,但代码能解决的问题有边界。如果推理里有"判断这句话是否有讽刺意味"这类语义操作呢?Python 解释器执行不了。
Chain of Code(Google DeepMind,ICML 2024 Oral)的洞察是:即使代码执行不了,也要用代码结构来表达推理过程,然后用语言模型来"模拟"执行。
CoC 引入了一个叫 LMulator(Language Model Emulator)的概念:
# CoC 生成的代码(混合了可执行代码和语义操作)
text = "这个产品真是太棒了,我都不想推荐给别人"
is_sarcastic = detect_sarcasm(text) # Python 没有这个函数
if is_sarcastic:
sentiment = "negative"
else:
sentiment = "positive"
print(sentiment)
这段代码里,detect_sarcasm(text) 是无法真正执行的语义函数。CoC 的做法:先尝试执行,如果执行失败(因为函数未定义),就让语言模型模拟这个函数的输出——也就是问模型"如果这个函数真的存在,给定这个输入,它应该返回什么?"
执行流程:
1. 尝试执行 detect_sarcasm("这个产品真是太棒了...")
2. NameError: detect_sarcasm is not defined
3. LMulator 介入:问 LLM "这句话有讽刺意味吗?" → True
4. 继续执行后续代码:is_sarcastic = True → sentiment = "negative"
5. 输出 "negative"
这个设计把"代码推理"的适用范围扩展到了语义任务。推理结构用代码表达(清晰、可组合),计算用解释器执行(准确),语义判断用 LLM 执行(灵活)。
CoC 的实验结果:在 BIG-Bench Hard 上,CoC 比 CoT 提升 12 个百分点(84% vs 72%)。在算术推理、符号推理、算法任务上都有明显提升,在纯语义任务上也能保持 CoT 的水平。
两者的实现对比
用一个完整的例子感受两者的差异:
任务:一个篮球队,本赛季 82 场打了 55 胜,主场胜率是 70%,客场打了 35 场,求主场场次和客场胜场数。
CoT 的做法:
思考:
- 总胜场 55,主场胜率 70%
- 主场场次 = 82 - 35 = 47
- 主场胜场 = 47 × 0.7 = 32.9 ≈ 33
- 客场胜场 = 55 - 33 = 22
答案:主场 47 场,客场胜 22 场
(这里 47 × 0.7 = 32.9,CoT 需要自己算,容易出错)
PoT 的做法:
total_games = 82
total_wins = 55
home_win_rate = 0.70
away_games = 35
home_games = total_games - away_games # 47
home_wins = round(home_games * home_win_rate) # 33
away_wins = total_wins - home_wins # 22
print(f"主场场次: {home_games}, 客场胜场: {away_wins}")
执行结果:主场场次: 47, 客场胜场: 22
计算由 Python 执行,精确。模型只需要把题目翻译成变量和公式,不需要在脑子里算 47 × 0.7。
CoC 的做法(如果任务包含"评估这支球队的斗志"这类语义问题):
team_data = {"wins": 55, "losses": 27, "home_wins": 33}
morale = evaluate_team_morale(team_data) # LMulator 处理
recommendation = "继续投资" if morale == "high" else "观望"
evaluate_team_morale 交给 LMulator 判断,其他部分真实执行。
代码作为推理接口的本质
PoT 和 CoC 暴露了一个更深的原则:语言模型最擅长把自然语言问题翻译成结构化表示,而不是执行结构化表示。
代码是一种结构化表示,它有几个关键性质使它特别适合作为推理接口:
形式化。代码的语义由执行语义定义,不模糊。a = b + c 就是赋值,没有歧义。CoT 里的自然语言步骤"把 b 和 c 加起来赋给 a"有很多合法的解读。
可执行。代码可以被真正运行,结果可以被客观验证。自然语言推理步骤只能被评价,不能被执行。
可组合。函数调用、变量引用、循环——这些结构让推理步骤之间的依赖关系显式化、可复用。
可调试。代码出错时有精确的错误信息(行号、错误类型)。自然语言推理出错时只能靠猜。
这四个性质解释了为什么代码比自然语言更适合作为 Agent 的推理载体——不只是"能执行数学",而是整个认知结构都更清晰、更可靠、更可调试。
局限与适用边界
PoT 和 CoC 不是万能的。几个真实的局限:
需要代码能力。生成正确代码要求模型有足够的编程能力。小模型(7B 及以下)在 PoT 上的提升不如大模型显著,因为它们本身生成代码的质量就不稳定。
任务必须有明确的"计算"。纯语言任务(写诗、情感分析、总结)用 PoT 不如 CoT,因为这些任务没有需要外包的数值计算。
执行环境的成本。PoT 需要一个代码执行环境(Python 解释器)。这增加了延迟,也增加了安全考虑(第 13 篇)。
错误传播。代码里的逻辑错误(不是算术错误)同样会产生错误结果,而且这类错误比 CoT 里的更难通过"看起来合理"来检测——代码运行了,给出了数字,看起来很自信。
这些局限直接导向了下一步演化:当代码生成本身可能有错误时,如何通过执行反馈来迭代修正。这是第 3 篇要讲的 CodeRL 和迭代自修正机制。
小结
PoT 和 CoC 做的一件事:把语言模型的推理和计算执行解耦。语言模型做翻译(自然语言 → 代码),解释器做执行(代码 → 结果)。LMulator 把这个模式扩展到语义任务。
这不是简单的"让 Agent 会用 Python",而是一种架构选择——代码作为推理接口,比自然语言更形式化、更可执行、更可调试。这个选择贯穿了整个 Code as Agent Harness 系列要讲的所有机制。
下一篇讲 CodeSteer:当语言模型在代码推理和文本推理之间需要动态切换时,怎么让它做出更好的选择——以及这个"选择"背后有多少被忽视的复杂性。
相关阅读
- Program of Thoughts Prompting (Chen et al., 2022) — PoT 原始论文
- Chain of Code (Li et al., 2023) — CoC + LMulator 原始论文,ICML 2024 Oral
- Program-Aided Language Models / PAL (Gao et al., 2022) — 与 PoT 同期的类似工作
- Chain-of-Thought Prompting (Wei et al., 2022) — CoT 原始论文,对比基准
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:01. 代码是推理的外包地:Program of Thoughts 与 Chain of Code
本文链接:https://www.sshipanoo.com/blog/ai/code-agent-harness/01-PoT-CoC/
本文最后一次更新为 天前,文章中的某些内容可能已过时!