跳转至

第 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

运行:

python examples/getting_started/step_03_prompt_and_context_builder.py

核心概念

这一节重点是建立这个心智:

  • prompt 不是散落在各处的字符串
  • 默认 prompt 来自配置 resolver
  • context 由 builder / provider / filter 组合出来

先区分两层问题

很多新人会把下面两件事混在一起:

  1. “基础 system prompt 是什么”
  2. “最终发给模型的完整上下文是怎么拼出来的”

在 jianmu 里,它们不是一回事:

  • system_prompt 是其中一个输入
  • ContextBuilder 负责把 system prompt、tools、skills、history、bootstrap 文件等内容装配成最终消息列表

这里有一个当前实现很关键的点:公开主路径里,LLM 类节点和预设通常不是每次都手写 ContextBuilder(...),而是通过 ContextBuilder.resolve_for_llm(...) 自动解析。你当然仍然可以直接 new 一个 builder,但默认路径已经是“按调用点解析 builder”。

这个示例会直接把三层都摆出来:

  1. 只改 system_prompt
  2. 直接构造 ContextBuilder(providers=[...])
  3. 手写一个最小 MessageProvider

但它的重点不是把三层都拿来跑完整 agent,而是先比较:

  • 最终 system prompt 是怎么组成的
  • 不同定制层到底接管了哪一部分

这个示例不会调用模型,只打印不同 builder 产生的 system message,专门用来观察 prompt 装配差异。

prompt 默认值从哪来

先看最常见的 ReAct 路径:

  • config/prompts.py 提供内置默认 prompt
  • jianmu.yaml 的 prompt 段可以覆盖这些默认文本
  • ReActConfig.system_prompt 可以在某个节点上继续做局部覆盖

所以如果你想“全项目都换一个默认 ReAct prompt”,优先改:

  • jianmu.yaml -> prompt.persona_default_prompt
  • jianmu.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 节点会按大致下面的顺序自动选:

  1. 先看节点是否显式传了 context_builder
  2. 再看 PromptRuntimeContext 里是否已经给了运行时 builder
  3. 如果当前调用点允许 skill-aware context,并且运行时明确请求了 skill-aware prompting,就走 for_skill(...)
  4. 如果调用方偏好 ReAct 语义,或者已经有 tools 描述,就走 for_react(...)
  5. 否则退回 for_chat(...)

这条规则很重要,因为它解释了两个常见现象:

  • 普通 LLM 节点默认不会因为“项目里有 skills”就自动看到 skill prompt
  • AgentLLMNode 这类调用点即使还没有具体 tool description,也会优先选 ReAct 预设
  • skill-aware prompting 只有调用方显式允许时才会出现,例如 SkillNode

这一节额外要理解什么

你应该开始区分三种场景:

  • for_chat:普通聊天,不暴露 tools 或 skills
  • for_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 定制的三层顺序

这一节还有一个很重要的实践顺序:

  1. 先用 system_prompt
  2. 不够时再上 ContextBuilder
  3. 还不够时才写自定义 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 顺序
  • 你是在自定义某个上下文块的生产方式

通常只有在前两层不够时,才需要走到这一步。

一个实用判断规则

如果你不确定该改哪一层,可以先按这个顺序判断:

  1. 只想改回答风格或 persona:改 system_prompt
  2. 想改上下文块的顺序、开关或组合:改 ContextBuilder
  3. 想改某一类上下文块本身的生成逻辑:写自定义 MessageProvider

这样做的好处是:

  • 先用最小改动解决问题
  • 不会过早跳到过重的抽象层
  • 后续排查 prompt 行为时也更容易定位问题出在哪一层

这一步学完后应该会什么

你应该能回答:

  • 我想全局改默认 prompt,应该改哪里
  • 我只想改某个节点的 system prompt,应该改哪里
  • 我什么时候该只传 system_prompt
  • 我什么时候该进一步自己构造 ContextBuilder

下一步