第 03 步:Prompt 与 ContextBuilder¶
这一节学什么¶
- prompt 默认值从哪里来
system_prompt、默认 prompt、provider blocks、ContextBuilder的关系ContextBuilder.for_chat / for_react / for_skill该怎么选
你会运行什么¶
示例代码:
examples/getting_started/step_03_prompt_and_context_builder.py
运行:
核心概念¶
这一节重点是建立这个心智:
- prompt 不是散落在各处的字符串
- 默认 prompt 来自配置 resolver
- context 由 builder / provider / filter 组合出来
先区分两层问题¶
很多新人会把下面两件事混在一起:
- “基础 system prompt 是什么”
- “最终发给模型的完整上下文是怎么拼出来的”
在 jianmu 里,它们不是一回事:
system_prompt是其中一个输入ContextBuilder负责把 system prompt、tools、skills、history、bootstrap 文件等内容装配成最终消息列表
这里有一个当前实现很关键的点:公开主路径里,LLM 类节点和预设通常不是每次都手写 ContextBuilder(...),而是通过 ContextBuilder.resolve_for_llm(...) 自动解析。你当然仍然可以直接 new 一个 builder,但默认路径已经是“按调用点解析 builder”。
这个示例会直接把三层都摆出来:
- 只改
system_prompt - 直接构造
ContextBuilder(providers=[...]) - 手写一个最小
MessageProvider
但它的重点不是把三层都拿来跑完整 agent,而是先比较:
- 最终 system prompt 是怎么组成的
- 不同定制层到底接管了哪一部分
这个示例不会调用模型,只打印不同 builder 产生的 system message,专门用来观察 prompt 装配差异。
prompt 默认值从哪来¶
先看最常见的 ReAct 路径:
config/prompts.py提供内置默认 promptjianmu.yaml的prompt段可以覆盖这些默认文本ReActConfig.system_prompt可以在某个节点上继续做局部覆盖
所以如果你想“全项目都换一个默认 ReAct prompt”,优先改:
jianmu.yaml -> prompt.persona_default_promptjianmu.yaml -> prompt.react_protocol_prompt
如果你只想“这个节点临时换一种口吻”,优先改:
ReActConfig.system_prompt
现在 ReAct 默认 prompt 的语义是什么¶
现在这条主路径的规则更直接:
system_prompt is None- 使用默认 persona prompt,而 ReAct 协议块仍由 ReAct 预设追加
system_prompt is not None- 使用你显式传入的 persona/base prompt,而 ReAct 协议块仍由 ReAct 预设追加
也就是说:
- 想使用框架默认 persona prompt,就不要传
system_prompt - 想接管这个节点的 persona/base prompt,就直接传自己的
system_prompt
这里要分清:
system_prompt控制的是 persona/base prompt 这一层- ReAct 协议块来自
ContextBuilder.for_react(...)/resolve_for_llm(prefer_react=True, ...)
ContextBuilder 到底在做什么¶
ContextBuilder 不是“又一个 prompt 字符串工具”。
它更像一个上下文装配器,负责决定最终消息流里出现什么内容。
最常见的组成包括:
- system prompt
- tools 描述块
- active skills 或 skills summary
- bootstrap 文件摘要
- 历史消息
- 最后的过滤步骤,例如消息数或 token 预算裁剪
也就是说,真正发给模型的通常不是单个 prompt 字符串,而是一组按顺序拼好的 Message。
ContextBuilder.resolve_for_llm(...) 的选择顺序¶
如果你没有显式传入 builder,LLM 节点会按大致下面的顺序自动选:
- 先看节点是否显式传了
context_builder - 再看
PromptRuntimeContext里是否已经给了运行时 builder - 如果当前调用点允许 skill-aware context,并且运行时明确请求了 skill-aware prompting,就走
for_skill(...) - 如果调用方偏好 ReAct 语义,或者已经有 tools 描述,就走
for_react(...) - 否则退回
for_chat(...)
这条规则很重要,因为它解释了两个常见现象:
- 普通 LLM 节点默认不会因为“项目里有 skills”就自动看到 skill prompt
AgentLLMNode这类调用点即使还没有具体 tool description,也会优先选 ReAct 预设- skill-aware prompting 只有调用方显式允许时才会出现,例如
SkillNode
这一节额外要理解什么¶
你应该开始区分三种场景:
for_chat:普通聊天,不暴露 tools 或 skillsfor_react:带 tools 的 Agent 主路径for_skill:给 skill-aware 执行准备上下文,包括 prompt skill 和 skill summary
可以先按下面选:
- 普通问答或单 LLM 聊天:
for_chat - 经典 ReAct agent:
for_react - 运行
SkillNode、或者你明确想把 skill 说明注入 prompt:for_skill
另外 for_skill(...) 现在有两类输入源:
- 显式传入的
skill_set - 或运行时 prompt 设置,例如
skills_catalog、skills_dir、include_skills_summary、include_active_skills
如果这两类都没有,普通 LLM 节点仍然只会落在 chat/ReAct 路径上。
这一步主要建立 prompt 装配心智,还不展开 SkillNode 的全部内部细节。
prompt 定制的三层顺序¶
这一节还有一个很重要的实践顺序:
- 先用
system_prompt - 不够时再上
ContextBuilder - 还不够时才写自定义
MessageProvider
可以把它理解成三档定制能力:
第一层:system_prompt¶
这是最轻量的定制方式。
适合:
- 只改 persona / base prompt
- 只想改变语气、角色、回答风格
- 不想碰框架已经提供的协议块
这一层的关键点是:
- 你改的是“模型应该以什么身份和口吻说话”
- tool protocol、skill block、history、memory 等上下文装配规则仍由框架保留
所以如果你的需求只是“更严谨一些”“更像产品经理一些”“回答更短一些”,通常先改 system_prompt 就够了。
第二层:ContextBuilder([...providers...])¶
这是中高级定制方式。
适合:
- 你想决定 system message、tool description、skill、history、memory 的组合顺序
- 你想插入额外的 prompt block
- 你想让多个节点复用同一套上下文装配策略
这一层的关键点是:
- 你不只是改一句 prompt
- 你是在决定“最终发给模型的上下文结构”
也就是说,ContextBuilder 解决的是“怎么装配”,不只是“写什么文案”。
第三层:自定义 MessageProvider¶
这是最强的定制方式。
适合:
- 你想完全控制某一类上下文怎么生成
- 你要从外部系统动态拉取内容
- 你需要自己的 skill/tool/history/memory 注入逻辑
这一层的关键点是:
- 你不再只是调整 builder 顺序
- 你是在自定义某个上下文块的生产方式
通常只有在前两层不够时,才需要走到这一步。
一个实用判断规则¶
如果你不确定该改哪一层,可以先按这个顺序判断:
- 只想改回答风格或 persona:改
system_prompt - 想改上下文块的顺序、开关或组合:改
ContextBuilder - 想改某一类上下文块本身的生成逻辑:写自定义
MessageProvider
这样做的好处是:
- 先用最小改动解决问题
- 不会过早跳到过重的抽象层
- 后续排查 prompt 行为时也更容易定位问题出在哪一层
这一步学完后应该会什么¶
你应该能回答:
- 我想全局改默认 prompt,应该改哪里
- 我只想改某个节点的 system prompt,应该改哪里
- 我什么时候该只传
system_prompt - 我什么时候该进一步自己构造
ContextBuilder