跳到主要内容

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.parseutils.py:218-221
4都失败 → 抛带「示范正确格式」的报错utils.py:224-251

第 4 级的报错很讲究:如果文本里同时出现 finalanswer,它会专门提示「你像是想给最终答案,应该这样写 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:1709local_python_executor.py:332):模型有时写成 final_answer = 42final_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)走经典工具调用:

维度CodeAgentToolCallingAgent
行动形式一段 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)。默认连 osopen 都不给。
  • 一步的返回观测会被截断(truncate_content,默认 2 万字符),防止超长输出撑爆上下文(agents.py:1752)。
  • managed agents + 远程执行不兼容:用远程执行器时若挂了子 agent 会直接报错(agents.py:1608-1609)。

9. 代码地图

主题文件符号
CodeAgent 主体src/smolagents/agents.pyCodeAgent
一步:生成→抠码→执行src/smolagents/agents.pyCodeAgent._step_stream
代码标签配置src/smolagents/agents.pyCodeAgent.__init__(code_block_tags)
系统提示装配src/smolagents/agents.pyCodeAgent.initialize_system_prompt
抠代码(多级兜底)src/smolagents/utils.pyparse_code_blobs / extract_code_from_text
final_answer 工具src/smolagents/default_tools.pyFinalAnswerTool
final_answer 异常包装src/smolagents/local_python_executor.pyFinalAnswerException / evaluate_python_code
误赋值修补src/smolagents/local_python_executor.pyfix_final_answer_code
传统工具调用 agentsrc/smolagents/agents.pyToolCallingAgent.process_tool_calls
系统提示模板src/smolagents/prompts/code_agent.yamlsystem_prompt