第 3 章 · 03 code_agent.yaml 四段提示词模板 ★


第 3 章 · 03 code_agent.yaml 四段提示词模板 ★

本节摘要:CodeAgent 的行为上限,几乎全部由一份 313 行的 YAML 文件决定——src/smolagents/prompts/code_agent.yaml。本节逐段精读它的四段模板:system_prompt(系统提示:角色声明 + 6 个 few-shot + 11 条代码规则)、planning(规划步三件套:initial_plan/update_plan_pre_messages/update_plan_post_messages)、managed_agent(子 agent 的任务下发与汇报)、final_answer(步数耗尽时的强行作答)。同时拆解 Jinja2 渲染变量(tools/managed_agents/authorized_imports/custom_instructions/code_block_tags),观察它如何与 populate_templateStrictUndefined 配合;最后对照 structured_code_agent.yaml 变体与 toolcalling_agent.yaml,总结 smolagents 提示词工程最鲜明的品质——克制:不堆角色扮演、不吹"世界级专家"(planning 段除外),把每一行 token 花在"可执行性与输出格式约束"上。

内容来源:原项目源码 src/smolagents/prompts/code_agent.yaml(313 行)、structured_code_agent.yaml(257 行)、toolcalling_agent.yaml(242 行),渲染逻辑在 src/smolagents/agents.pypopulate_template 与两个 initialize_system_prompt

⚠️ 注意:四段模板在代码里对应 agents.py:166-180PromptTemplates TypedDict(system_prompt/planning/managed_agent/final_answer),自定义模板必须四段齐全——__init__ 里的断言会逐 key 检查,缺一个直接报错。想改提示词,最稳的方式是 yaml.safe_load 原文件 → 改字段 → 整体传入 prompt_templates=

学习目标

阅读完本节,你应当能够:

  1. 说出四段模板各自的触发时机:system_prompt(每次 run 开头)、planning(每 planning_interval 步)、managed_agent(被 manager 调用时)、final_answer(撞 max_steps 时)。
  2. 逐条解释 system_prompt 的 11 条代码规则分别防什么错。
  3. 说清 6 个 few-shot 示例的递进设计(单工具→链式→重试→循环→计算)。
  4. 列出全部 Jinja2 渲染变量及来源。
  5. 解释 {{code_block_opening_tag}} 参数化标签的意义与 StrictUndefined 的防呆作用。
  6. 描述 structured_code_agent.yaml 改了什么、何时启用。

一、四段模板的骨架与渲染机制

文件顶层就是四个 YAML key,与 PromptTemplates TypedDict 一一对应:

system_prompt: |- # 每次 run() 渲染并写入 SystemPromptStep You are an expert assistant who can solve any task using code blobs. ... planning: # planning_interval 触发规划步时渲染 initial_plan : |- update_plan_pre_messages: |- update_plan_post_messages: |- managed_agent: # 本 agent 作为子 agent 被 manager 调用时渲染 task: |- report: |- final_answer: # 步数耗尽 _handle_max_steps_reached 时渲染 pre_messages: |- post_messages: |-

渲染入口在 CodeAgent.initialize_system_prompt(agents.py:1620-1636):

1621 system_prompt = populate_template( 1622 self.prompt_templates["system_prompt"], 1623 variables={ 1624 "tools": self.tools, 1625 "managed_agents": self.managed_agents, 1626 "authorized_imports": ( 1627 "You can import from any package you want." 1628 if "*" in self.authorized_imports else str(self.authorized_imports) 1629 ), 1632 "custom_instructions": self.instructions, 1633 "code_block_opening_tag": self.code_block_tags[0], 1634 "code_block_opening_tag": ..., "code_block_closing_tag": self.code_block_tags[1], 1635 }, 1636 )

populate_template(agents.py:102-107)内部是 jinja2.Template(template, undefined=StrictUndefined)——任何模板里引用了但没传的变量都会抛异常,而不是像默认 Jinja2 那样静默渲染成空串。这保证了"模板改了、调用点忘传变量"这类错误在第一次渲染时就炸出来。

全部变量一览:tools(每个 Tool 经 to_code_prompt() 展开为 def name(args) -> type: """description""" 风格的函数签名)、managed_agents(同款函数签名,task: str, additional_args: dict 两个参数)、authorized_imports(白名单列表或"任意包"声明)、custom_instructions(用户 instructions 追加)、code_block_opening_tag/code_block_closing_tag(代码块边界,标签本身被参数化)。

看一个"渲染后长什么样"的具体例子。假设 tools=[WebSearchTool()],模板里的工具循环段:

{{code_block_opening_tag}} {%- for tool in tools.values() %} {{ tool.to_code_prompt() }} {% endfor %} {{code_block_closing_tag}}

实际渲染成(示意):

<code> def web_search(query: str) -> str: """Performs a web search with your query and returns the string results.""" </code>

工具在 CodeAgent 的系统提示里以 Python 函数签名而非 JSON schema 的面貌出现——与"工具就是命名空间里的函数"的执行模型完全一致,LLM 从提示词到代码执行看到的是同一个世界观。这是 CodeAgent 提示词与 ToolCallingAgent(用 to_tool_calling_prompt() 渲染成条目列表)最深的一处分野。

二、system_prompt 段:角色 + 示例 + 11 条规则

1. 角色与协议(第 1-10 行)

You are an expert assistant who can solve any task using code blobs. To do so, you have been given access to a list of tools: these tools are basically Python functions which you can call with code. To solve the task, you must plan forward to proceed in a series of steps, in a cycle of Thought, Code, and Observation sequences. At each step, in the 'Thought:' sequence, you should first explain your reasoning ... Then in the Code sequence you should write the code in simple Python. The code sequence must be opened with '{{code_block_opening_tag}}', and closed with '{{code_block_closing_tag}}'. During each intermediate step, you can use 'print()' to save whatever important information you will then need. These print outputs will then appear in the 'Observation:' field, ... In the end you have to return a final answer using the `final_answer` tool.

五行讲清五件事:工具是 Python 函数、节奏是 Thought/Code/Observation 循环、代码块怎么包裹、print 是"跨步记忆通道"(上节源码里 Observation=执行日志的提示词侧呼应)、final_answer 收尾。注意 {{code_block_opening_tag}}:默认渲染成 <code>,markdown 模式渲染成 ```python——模板与解析器(parse_code_blobs)共用同一对标签配置,永远对得上。

2. 六个 few-shot 示例(第 12-131 行,节选)

示例按"能力难度"递进:①document_qa→image_generator(两工具链式,展示变量在两步间传递);②纯算术(零工具,纯计算);③translator→image_qa(additional_args 变量直接当 Python 变量用);④Ulam 访谈(搜索无果→换宽泛查询→循环访问网页——失败重试与 for 循环示范,四个示例里最长);⑤城市人口(一个 for 循环并行查两个城市,上节步数对比用一个);⑥教皇年龄幂运算(交叉验证:wiki 与 web_search 互证后再算)。每个示例都是完整的 Thought/Code/Observation 三元组,Observation 里故意放了"No result found"、"truncated" 这样的真实噪声——教模型面对不完美观察。结尾一句免责:"Above examples were using notional tools that might not exist for you"——防止模型照抄示例里的幻觉工具名。

3. 11 条代码规则(第 157-168 行)

1. Always provide a 'Thought:' sequence, and a '{{code_block_opening_tag}}' sequence ending with '{{code_block_closing_tag}}', else you will fail. # 格式底线 2. Use only variables that you have defined! # 防幻觉变量 3. Always use the right arguments for the tools. DO NOT pass the arguments as a dict ..., but use the arguments directly ... # 防 kwargs 打包成 dict 4. For tools WITHOUT JSON output schema: Take care to not chain too many sequential tool calls in the same code block ... # 无 schema 工具别深链 5. For tools WITH JSON output schema: You can confidently chain multiple tool calls and directly access structured output fields ... # 有 schema 可放心链 6. Call a tool only when needed, and never re-do a tool call that you previously did with the exact same parameters. # 防重复调用烧步数 7. Don't name any new variable with the same name as a tool: for instance don't name a variable 'final_answer'. # 与 fix_final_answer_code 呼应 8. Never create any notional variables in our code ... # 防编造变量名进日志 9. You can use imports in your code, but only from the following list of modules: {{authorized_imports}} # import 白名单 10. The state persists between code executions: so if in one step you've created variables or imported modules, these will all persist. # 跨步状态(变量复用!) 11. Don't give up! You're in charge of solving the task, not providing directions to solve it. # 反偷懒

每条规则都对应一类真实失败模式:规则 3 防的是"把参数写成 wikipedia_search({'query': ...})"(LLM 常见惯性);规则 4/5 把"能否深链"交给工具的 JSON schema 有无(与第 5 章 Tool 的 output_type 机制联动);规则 7 与第 1 节读过的 fix_final_answer_code 正则修复构成"提示词预防 + 代码兜底"双保险;规则 9 直接渲染白名单进提示词——授权清单模型看得见,解释器才拦得住;规则 10 是"变量复用"优势的提示词侧声明。

三、planning 段:规划步三件套

planning_interval=N 时每 N 步插入规划步(第 4 章精读运行时序),模板三个字段:

  • initial_plan:开篇那句 "You are a world expert at analyzing a situation..."(全文件唯一的头衔加冕)随后是严格的两段式输出格式:先"Facts survey"(1.1 已知事实/1.2 待查事实/1.3 待推导事实),再"Plan"(只列高层步骤,明确禁止写出具体工具调用,原话 "DO NOT DETAIL INDIVIDUAL TOOL CALLS"),以 <end_plan> 标签结束——该标签同时是 model.generate(stop_sequences=["<end_plan>"]) 的停止序列,规划不烧多余 token。注意工具列表在规划段也会渲染一遍(planning 是独立的 LLM 调用,不继承 system_prompt),但没有代码块标签规则——规划只输出文字计划,不写代码。
  • update_plan_pre_messages:插在记忆消息,告知"以下是历史尝试,可续可推倒重来"(原话 "If you are stalled, you can make a completely new plan starting from scratch")。
  • update_plan_post_messages:插在记忆消息,要求输出"更新版事实清单"(四小节,新增 "Facts that we have learned" 一节,让已完成的探索沉淀)与新计划,并渲染 {remaining_steps} 让模型知道还剩几步可花——预算意识直接影响计划的取舍粒度。

四、managed_agent 与 final_answer 段

  • managed_agent.task:本 agent 被当工具调用时(__call__,agents.py:868-890),manager 的任务先套此模板:"You're a helpful agent named '{{name}}'... You're helping your manager solve a wider task"——关键是强制三段式汇报(简版结论/详版结论/附加上下文),因为"everything that you do not pass as an argument to final_answer will be lost"(子 agent 的记忆不会自动回流,必须塞进 final_answer)。
  • managed_agent.report:manager 收到结果时的包装:"Here is the final answer from your managed agent '{{name}}': {{final_answer}}"。
  • final_answer.pre/post_messages:撞 max_steps_handle_max_steps_reached 用它们做最后一次"抢救性作答":pre 声明"agent 卡住了,这是它的记忆",post 要求"基于以上记忆回答用户任务"——即使失败,用户也拿到一个有依据的答案而非裸异常。

五、structured_code_agent.yaml 与两模板对比

CodeAgent(use_structured_outputs_internally=True) 时改用 structured_code_agent.yaml(257 行)。差异集中在协议声明与示例:Thought/Code 不再是文本序列,而是要求模型输出固定 JSON 结构:

{"thought": "...", "code": "print('hello')"}

few-shot 相应改为 JSON 行(代码作为字符串值嵌在 "code" 键里)。第 1 节源码 1704-1706 行已见其解析:json.loads(output_text)["code"],再 extract_code_from_text 兜一层标签。为什么有这个变体:很多推理后端(OpenAI response_format/vLLM guided decoding)对结构化输出有约束解码加持,格式可靠性高于自由文本,对中小模型尤其提分(源码 docstring:"improves performance for many models")。

toolcalling_agent.yaml 的对比一句话总结:骨架完全同构(角色/示例/工具循环/规则四件套),差异只在"动作的语法"——一边教 <code> 代码块 + 11 条代码规则,一边教 {"name":..., "arguments":...} blob + 4 条调用规则;一边的示例展示循环与变量,一边的示例展示多轮 Action/Observation。工具列表的渲染函数也不同:to_code_prompt()(函数签名风格)vs to_tool_calling_prompt()(列表条目风格)。

六、自定义模板的正确姿势

两类需求两条路:

只想追加几条要求——用 instructions 参数,它会渲染进 system_prompt 末尾的 custom_instructions 槽位(模板 170-172 行):

agent = CodeAgent( tools=[WebSearchTool()], model=model, instructions="Always cite sources in your final answer. Reply in Chinese.", )

想动结构本身——整包替换 prompt_templates。四段齐全是硬约束,所以推荐"load 原文件→改字段→传入":

import yaml, importlib.resources path = importlib.resources.files("smolagents.prompts").joinpath("code_agent.yaml") templates = yaml.safe_load(path.read_text()) templates["system_prompt"] = templates["system_prompt"].replace( "Now Begin!", "现在开始!并始终用中文思考。" ) # 或整体重写该段,保留 {{tools}}/{{authorized_imports}} 等槽位 agent = CodeAgent(tools=[...], model=model, prompt_templates=templates)

三个守则:①保留全部 Jinja2 槽位(StrictUndefined 会替你把关,漏删变量当场报错);②若改了代码块标签,记得同步 code_block_tags 参数,保持模板与 parse_code_blobs 一致;③few-shot 是行为的最强杠杆——想教新行为,加示例比加规则有效。改完可临时把 memory.system_prompt 打印出来人工过目,这是最朴素也最有效的提示词 review。

💡 阶梯要点:smolagents 的提示词哲学是把提示词当 API 文档写,不当人格剧本写:角色一句话、能力全靠示例教、边界全靠规则划、变量全部 StrictUndefined 校验。313 行里没有一句"你是世界级 XX 专家"式的打气(planning 段那句 world expert 是唯一例外),每一行都在回答"模型下一步该输出什么、什么算错"。改提示词时守同样的纪律:先问"这条规则防的是哪个真实错误",防不出具体错误的句子一律删。

本节要点回顾

  1. 四段模板 = system_prompt(run 开头)/ planning(每 N 步)/ managed_agent(被调用时)/ final_answer(步数耗尽时),自定义必须四段齐全否则断言失败。
  2. system_prompt 三层结构:协议声明(Thought/Code/Observation + print 通道)→ 6 个递进 few-shot → 11 条规则。
  3. 11 条规则各防一类真实失败(幻觉变量/dict 打包参数/重复调用/变量遮蔽工具名/越权 import/放弃)。
  4. 渲染变量:tools/managed_agents/authorized_imports/custom_instructions/code_block 标签对;StrictUndefined 让漏传变量当场炸出。
  5. 代码块标签参数化,模板与解析器永远同步;白名单直接渲染进提示词;工具以函数签名出现,与"工具即函数"的执行世界观一致。
  6. planning 三件套:initial_plan(两段式 + <end_plan> 停止序列)/pre/post(夹住记忆,含 remaining_steps 与"已学到的事实"一节)。
  7. managed_agent 段强制三段式汇报(final_answer 之外皆丢失);final_answer 段做步数耗尽的抢救作答。
  8. structured 变体把协议改成 {"thought","code"} JSON,借约束解码提格式可靠性;与 toolcalling 模板骨架同构、只换动作语法。
  9. 自定义两条路:轻量用 instructions 槽位;结构级改用"load 原文件→改字段→传入",保留全部 Jinja2 槽位。

下一章离开"动作的语言",进入攀爬装备区——第 4 章精读记忆与规划:AgentMemory 的六种 Step、write_memory_to_messages 的细节与 planning_interval 的完整运行时序。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U