CodeAgent:把「行动」写成代码
本章讲 smolagents 的招牌:一步的「行动」不是一次工具调用,而是一整段 Python 代码。我们看它怎么让模型产出代码、怎么把代码抠出来、怎么靠
final_answer终止。末尾对比ToolCallingAgent。
1. 它要解决的小问题
传统工具调用一步只能干一件事:模型输出 {"tool":"search","args":{...}},系统执行,再回一轮。要「搜三个关键词、各取首条、拼起来」就得三四轮往返,既慢又笨。
CodeAgent 的答案:让模型直接写代码。 一步之内它能连调多个工具、把结果存进变量、写 for 循环和 if 判断——这些都是代码天然就有的表达力。
作者的论据(README):动作用代码表达比用 JSON 更自然、更省步数,因为代码本就是为「组合调用 + 控制流」设计的。
2. 思路/直觉
工具在 CodeAgent 眼里就是预先注入执行环境的 Python 函数。模型写:
# 示意,非源码
pages = web_search(query="1979 interview Ulam Sherwin Einstein")
print(pages) # print 的东西会当「观测」回喂
answer = extract_answer(pages)
final_answer(answer) # 宣布完成
系统把这段代码跑在一个受限解释器里(见 03-local-python-executor.md),print 的内容和最后返回值作为「观测」回灌记忆。final_answer(...) 是特殊工具,一旦调用就终止整个循环。
3. 一步在 CodeAgent 里怎么走
怎么读:这是 CodeAgent._step_stream(agents.py:1638)从上到下的顺序。
① 记忆 → 消息 write_memory_to_messages()
② 定停止序列 ["Observation:", "Calling tools:", 结束标签]
③ 模型生成 model.generate(...) 或 generate_stream(...)
④ 补上闭合标签 若输出没以 </code> 收尾,补一个(诱导模型早停)
⑤ 抠代码 parse_code_blobs(output, code_block_tags)
⑥ 修 final_answer fix_final_answer_code(...)
⑦ 执行 code_output = python_executor(code_action)
⑧ 观测 "Execution logs:\n" + 日志 + "Last output:\n" + 返回值
⑨ 产出 ActionOutput(output, is_final_answer=code_output.is_final_answer)
4. 关键机制一:怎么可靠地抠出代码
模型的回答是自由文本,代码混在「思考」里。CodeAgent 用成对标签框定代码区。默认标签是 <code> / </code>;也可切成 markdown 的 ```python / ```(agents.py:1556-1564,code_block_tags)。
抠取逻辑在 parse_code_blobs(utils.py:198),是一套多级兜底:
| 级别 | 尝试什么 | 依据 |
|---|---|---|
| 1 | 用配置的标签正则抓 <code>…</code> | extract_code_from_text(utils.py:189) |
| 2 | 抓不到 → 退回 markdown ```python 围栏 | utils.py:213-214 |
| 3 | 还抓不到 → 试着把整段文本当代码 ast.parse | utils.py:218-221 |
| 4 | 都失败 → 抛带「示范正确格式」的报错 | utils.py:224-251 |
第 4 级的报错很讲究:如果文本里同时出现 final 和 answer,它会专门提示「你像是想给最终答案,应该这样写 final_answer(...)」。这类面向模型的纠错提示是全库反复出现的手法——报错本身就是给模型的教学。
停止序列的巧思(agents.py:1651-1654):把 </code> 加进停止序列,模型一写完代码块就停,不浪费 token 继续编「观测」。反过来,_step_stream 里若模型输出没以闭合标签结尾,会手动补上并写回历史(agents.py:1690-1695),既保证能抠出代码,又把这个结尾「示范」给后续调用,诱导模型养成早停习惯。
5. 关键机制二:final_answer 怎么终止循环
final_answer 是一个普通工具(default_tools.py:83,FinalAnswerTool,forward 就是原样返回入参)。难点是:它在用户代码内部被调用,系统怎么知道「这一步该结束整个 run」?
答案是用异常穿透。执行代码前,evaluate_python_code 会把 final_answer 包一层(local_python_executor.py:1630-1636):
# 源码要点,local_python_executor.py:1633
def final_answer(*args, **kwargs):
raise FinalAnswerException(previous_final_answer(*args, **kwargs))
执行循环捕获这个异常,把它的值当作「最终答案」并置 is_final_answer=True(local_python_executor.py:1649-1654)。
为什么用异常、而且继承 BaseException 而非 Exception?(local_python_executor.py:1572-1577)因为模型写的代码里常有 try: ... except Exception: ...。如果 FinalAnswerException 是普通 Exception,模型代码里一个宽泛的 except 就会把「我要交答案」这个信号吃掉。继承 BaseException 让它绕过所有常规 except,稳稳穿透到执行器顶层。这是个很妙的边界设计。
还有一个小修补 fix_final_answer_code(agents.py:1709、local_python_executor.py:332):模型有时写成 final_answer = 42 又 final_answer(...),把函数名覆盖成了变量。这个函数用正则把这类误赋值修回来。
6. 关键机制三:结构化输出的可选路径
如果开 use_structured_outputs_internally,CodeAgent 不靠标签抠代码,而是让模型返回 JSON({"thought":..., "code":...}),直接取 code 字段(agents.py:1704-1706),并换用 structured_code_agent.yaml 系统提示。适合原生支持 JSON schema 约束输出的模型。
7. 对比:ToolCallingAgent(传统路线)
同一个基类的另一个子类 ToolCallingAgent(agents.py:1215)走经典工具调用:
| 维度 | CodeAgent | ToolCallingAgent |
|---|---|---|
| 行动形式 | 一段 Python 代码 | 模型原生 tool_calls(JSON) |
| 一步能做几件事 | 多个工具 + 变量 + 控制流 | 通常一个/几个并行工具调用 |
| 怎么执行 | 受限 Python 解释器 | 直接 tool(**args) 调函数 |
| 并行 | 由代码自身表达 | ThreadPoolExecutor 并行多个调用 |
| 终止 | 代码里调 final_answer → 抛异常 | 工具名 == final_answer |
| 依赖 | 不要求模型支持 function calling | 依赖模型的 tool-calling 能力 |
ToolCallingAgent 的一步在 _step_stream(agents.py:1276)+ process_tool_calls(agents.py:1361):模型没返回 tool_calls 时还能从文本里解析(parse_tool_calls);多个调用用线程池并行执行(agents.py:1424-1434),并用 copy_context() 保证每个线程有独立上下文。
8. 边界与坑
- CodeAgent 只把工具「函数化」注入,不给全 Python。 能 import 什么、能调什么内建,由执行器的白名单决定(见
03)。默认连os、open都不给。 - 一步的返回观测会被截断(
truncate_content,默认 2 万字符),防止超长输出撑爆上下文(agents.py:1752)。 - managed agents + 远程执行不兼容:用远程执行器时若挂了子 agent 会直接报错(
agents.py:1608-1609)。
9. 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| CodeAgent 主体 | src/smolagents/agents.py | CodeAgent |
| 一步:生成→抠码→执行 | src/smolagents/agents.py | CodeAgent._step_stream |
| 代码标签配置 | src/smolagents/agents.py | CodeAgent.__init__(code_block_tags) |
| 系统提示装配 | src/smolagents/agents.py | CodeAgent.initialize_system_prompt |
| 抠代码(多级兜底) | src/smolagents/utils.py | parse_code_blobs / extract_code_from_text |
| final_answer 工具 | src/smolagents/default_tools.py | FinalAnswerTool |
| final_answer 异常包装 | src/smolagents/local_python_executor.py | FinalAnswerException / evaluate_python_code |
| 误赋值修补 | src/smolagents/local_python_executor.py | fix_final_answer_code |
| 传统工具调用 agent | src/smolagents/agents.py | ToolCallingAgent.process_tool_calls |
| 系统提示模板 | src/smolagents/prompts/code_agent.yaml | system_prompt |