先说结论 :Deep Agents 的 System Prompt 不是一段文本,而是一场四层卷饼------USER 卷在最外面,BASE 或 CUSTOM 是馅料,SUFFIX 是酱汁,Memory 是餐后甜点。每一层都有不可违反的座位号,搞错顺序,模型的行为就会"跑偏"到你怀疑人生。
保证看完这篇,你去面试能从"System Prompt 就是给模型写一段话"升级到"System Prompt 是一套精密的分层注入协议"------而且能讲清楚为什么 USER 要坐第一排、SUFFIX 要坐最后一排、Memory 为什么是最贵的餐后甜点。
🌯 做菜比喻:四层卷饼协议
想象你在卷一个 Mission-style Burrito(旧金山大卷饼):
| 层 | 卷饼里的角色 | Deep Agents 里的角色 | 一句话 |
|---|---|---|---|
| USER | 你点的特殊要求("不要香菜加辣") | system_prompt 参数 |
永远最外面、最先被看到,你的意志就是最高指令 |
| BASE | 标准馅料(米饭+豆子+肉) | BASE_AGENT_PROMPT 常量 |
SDK 的默认行为规范,每次都有 |
| CUSTOM | 你自带馅料("用我家的肉酱") | HarnessProfile.base_system_prompt |
完全替换 BASE,不是追加 |
| SUFFIX | 酱汁(永远最后淋) | HarnessProfile.system_prompt_suffix |
模型调优指导,永远坐最后排 |
| Memory | 餐后甜点(单独上桌) | MemoryMiddleware 注入 |
动态加载的 <agent_memory> 标签 |
两个不可违反的不变量(Invariant):
- USER 永远在最前面------调用者的指令优先级最高,无论你用什么模型。
- SUFFIX 永远在最后面------模型调优指导要离对话历史最近(模型的注意力在尾部最高)。
这两个不变量不是"建议",是源码里硬编码的 ------graph.py 第 730-735 行,三行代码,没给你留改的余地。
📜 第一幕:BASE_AGENT_PROMPT------那碗标准馅料
每个 Deep Agent 一出生,就自带一份"出厂设定"------BASE_AGENT_PROMPT。它定义在 graph.py 里,是这么一坨(精简版):
python
BASE_AGENT_PROMPT = """You are a deep agent, an AI assistant that helps users
accomplish tasks using tools. You respond with text and tool calls.
The user can see your responses and tool outputs in real time.
## Core Behavior
- Be concise and direct. Don't over-explain unless asked.
- NEVER add unnecessary preamble ("Sure!", "Great question!", "I'll now...").
- Don't say "I'll now do X" --- just do it.
- If the request is underspecified, ask only the minimum followup needed.
- If asked how to approach something, explain first, then act.
## Professional Objectivity
- Prioritize accuracy over validating the user's beliefs
- Disagree respectfully when the user is incorrect
- Avoid unnecessary superlatives, praise, or emotional validation
## Doing Tasks
When the user asks you to do something:
1. **Understand first** --- read relevant files, check existing patterns.
2. **Act** --- implement the solution. Work quickly but accurately.
3. **Verify** --- check your work against what was asked.
Keep working until the task is fully complete. Don't stop partway.
## Clarifying Requests
- Do not ask for details the user already supplied.
- Use reasonable defaults when the request clearly implies them.
- Ask domain-defining questions before implementation questions.
## Progress Updates
For longer tasks, provide brief progress updates at reasonable intervals.
"""
注意 :这不是随便写的鸡汤。这是一份行为约束协议:
- "Don't say I'll now do X --- just do it" → 反废话
- "Prioritize accuracy over validating the user's beliefs" → 反谄媚
- "Disagree respectfully when the user is incorrect" → 反应声虫
- "Keep working until the task is fully complete" → 反摆烂
如果你用过 Claude,你会觉得这些规则很眼熟------因为这就是 Claude 的"出厂性格",Deep Agents 把它硬编码成了 SDK 的一部分。
🔥 如果面试官问你:BASE 和 CUSTOM 到底什么关系?
答 :CUSTOM 是替换 ,不是追加。设了 base_system_prompt,BASE_AGENT_PROMPT 就完全不参与组装了。就像你自带肉酱,厨师不会再往卷饼里放标准肉馅------否则味道打架。
源码证据在 _apply_profile_prompt 函数:
python
def _apply_profile_prompt(profile: HarnessProfile, base_prompt: str) -> str:
# 👇 注意这个 if/else:CUSTOM 设了就彻底替换 BASE
prompt = profile.base_system_prompt if profile.base_system_prompt is not None else base_prompt
if profile.system_prompt_suffix is not None:
prompt = prompt + "\n\n" + profile.system_prompt_suffix
return prompt
一行 if/else,两个世界的命运就此分岔。
🎭 第二幕:HarnessProfile------模型的人格面具
HarnessProfile 是 Deep Agents 最精妙的设计之一------同一个 SDK,不同模型,不同的"出厂调校"。
Profile 是什么?
一句话:Profile = 模型的人格面具。它不改变模型的权重,但改变了模型在 Deep Agents 框架里的"行为指南"。
python
@dataclass(frozen=True)
class HarnessProfile:
base_system_prompt: str | None = None # CUSTOM 槽------替换 BASE
system_prompt_suffix: str | None = None # SUFFIX 槽------追加到最后
tool_description_overrides: Mapping[str, str] = field(default_factory=dict)
excluded_tools: frozenset[str] = frozenset()
excluded_middleware: frozenset[type | str] = frozenset()
extra_middleware: Sequence[AgentMiddleware] | Callable = ()
general_purpose_subagent: GeneralPurposeSubagentProfile | None = None
其中跟 System Prompt 直接相关的只有两个:base_system_prompt 和 system_prompt_suffix。其余字段控制的是工具、中间件和子智能体------那些是"硬件配置",不是"软件指令"。
Profile 的注册与查找
Profile 注册到一个全局字典里,key 的格式是 provider:model(如 "anthropic:claude-sonnet-4-6")或裸 provider(如 "anthropic")。
查找顺序(三级回退):
markdown
1. 精确匹配:"anthropic:claude-sonnet-4-6" → 有就用
2. Provider 前缀:"anthropic" → 有就用
3. None → 空的默认 Profile
当精确匹配和 Provider 匹配同时存在 时,会合并(merge),不是替换。合并规则:
| 字段 | 合并策略 |
|---|---|
base_system_prompt |
Override 优先(Override 设了就用 Override 的,没设就继承 Base 的) |
system_prompt_suffix |
Override 优先 |
tool_description_overrides |
按 key 合并(Base 设 "task",Override 设 "ls",两个都保留;同名 key Override 赢) |
excluded_tools |
并集({"execute"} ∪ {"grep"} = {"execute", "grep"}) |
excluded_middleware |
并集 |
extra_middleware |
按类型合并(Override 的同类实例替换 Base 的,新类追加) |
这个合并逻辑在 _merge_profiles 函数里,30 行代码,干净利落:
python
def _merge_profiles(base: HarnessProfile, override: HarnessProfile) -> HarnessProfile:
return HarnessProfile(
base_system_prompt=(
override.base_system_prompt if override.base_system_prompt is not None
else base.base_system_prompt
),
system_prompt_suffix=(
override.system_prompt_suffix if override.system_prompt_suffix is not None
else base.system_prompt_suffix
),
tool_description_overrides={
**base.tool_description_overrides,
**override.tool_description_overrides,
},
excluded_tools=base.excluded_tools | override.excluded_tools,
excluded_middleware=base.excluded_middleware | override.excluded_middleware,
extra_middleware=_merge_middleware(base.extra_middleware, override.extra_middleware),
general_purpose_subagent=_merge_general_purpose_subagent_profiles(...),
)
🔥 如果面试官问你:为什么 excluded_tools 用并集而不是 Override 优先?
答 :安全性。如果 Base Profile 排除了 execute(安全风险),Override Profile 只排除了 grep,你要的是"两个都别出现"------不是"Override 没提 execute 就把 execute 放回来"。并集 = 只会更严格,不会更宽松。这是安全设计的黄金法则:只能加锁,不能解锁。
🏷️ 第三幕:内置 Profile 逐个拆------每个模型的"出厂 SUFFIX"长什么样?
Deep Agents 内置了四个 HarnessProfile,全部只设了 system_prompt_suffix(没有 base_system_prompt)------也就是说,它们不替换 BASE,只追加一段"模型调优指导"。
Claude Sonnet 4.6 / Haiku 4.5------同一个 SUFFIX
Sonnet 4.6 和 Haiku 4.5 共享完全相同的 SUFFIX,由三个 XML 标签组成:
xml
<use_parallel_tool_calls>
If you intend to call multiple tools and there are no dependencies between
the tool calls, make all of the independent tool calls in parallel.
Prioritize calling tools simultaneously whenever the actions can be done
in parallel rather than sequentially.
</use_parallel_tool_calls>
<investigate_before_answering>
Never speculate about code you have not opened. If the user references a
specific file, you MUST read the file before answering. Make sure to
investigate and read relevant files BEFORE answering questions about the codebase.
</investigate_before_answering>
<tool_result_reflection>
After receiving tool results, carefully reflect on their quality and
determine optimal next steps before proceeding.
</tool_result_reflection>
翻译成人话:
| 标签 | 一句话 | 解决的问题 |
|---|---|---|
<use_parallel_tool_calls> |
能并行就并行 | Claude 默认倾向于串行调用工具,这个标签告诉它"别等了,一起上" |
<investigate_before_answering> |
先看代码再说话 | 防止 Claude 不看代码就编答案(幻觉) |
<tool_result_reflection> |
看完结果再行动 | 防止 Claude 拿到工具结果就急着回答,而不消化内容 |
Claude Opus 4.7------额外的两个标签
Opus 4.7 在 Claude 通用三件套之上,多了两个标签:
xml
<tool_usage>
When a task depends on the state of files, tests, or system output,
use tools to observe that state directly rather than reasoning from
memory about what it probably contains. Read files before describing them.
Run tests before claiming they pass.
</tool_usage>
<subagent_usage>
Do not spawn a subagent for work you can complete directly in a single
response (e.g. refactoring a function you can already see).
Spawn multiple subagents in the same turn when fanning out across items
or reading multiple files.
</subagent_usage>
为什么 Opus 需要额外指导?因为 Opus 的行为模式和 Sonnet 不同:
- Opus 倾向于"少用工具" →
<tool_usage>鼓励它主动调查 - Opus 倾向于"少派子智能体" →
<subagent_usage>告诉它该派就派
这不是瞎猜的------源码注释明确写了:
"Claude Opus 4.7-specific overlays that counter the model's documented tendency to use tools and spawn subagents less aggressively than prior Opus generations."
翻译:Opus 4.7 是个"佛系员工",不催不动手,不催不派活。这两个标签就是它的 KPI 考核表。
OpenAI Codex------完全不同的 SUFFIX
Codex 模型有自己的完整 SUFFIX,和 Claude 风格完全不同:
markdown
## Codex-Specific Behavior
- You are an autonomous senior engineer. Once given a direction, proactively
gather context, plan, implement, and verify without waiting for additional
prompts at each step.
- Persist until the task is fully handled end-to-end within the current turn
whenever feasible.
- Bias to action: default to implementing with reasonable assumptions.
Do not end your turn with clarifications unless truly blocked.
- Do not communicate an upfront plan or status preamble before acting. Just act.
## Parallel Tool Use
- Before any tool call, decide ALL files and resources you will need.
- Batch reads, searches, and other independent operations into parallel
tool calls instead of issuing them one at a time.
## Plan Hygiene
- Before finishing, reconcile every TODO or plan item created via write_todos.
Mark each as done, blocked, or cancelled. Do not finish with pending items.
对比:
| 维度 | Claude Profile | Codex Profile |
|---|---|---|
| 语气 | XML 标签包裹,像 API 文档 | Markdown 标题,像内部规范 |
| 核心关注 | 防幻觉、防串行 | 促自主、促行动 |
| 工具使用 | "能并行就并行" | "先想好全要什么再调" |
| 子智能体 | Opus 专属提及 | 压根没提 |
| TODO 管理 | 没提 | 专门有个 "Plan Hygiene" 段 |
这就是为什么 Deep Agents 需要 Profile------不同模型出厂"性格"不同,需要不同的行为矫正。
🔥 如果面试官问你:为什么内置 Profile 全都只设 SUFFIX,不设 CUSTOM?
答:替换 BASE 是极端操作。BASE 定义的是 Deep Agent 的"通用行为规范"(反废话、反谄媚、反摆烂),这些规则对所有模型都适用。SUFFIX 是"增量调整"------在通用规范之上加模型专属的行为矫正。只有当你完全不需要 SDK 的默认行为时才用 CUSTOM------那基本意味着你在写一个完全不同的 Agent,而不是 Deep Agent 了。
🏗️ 第四幕:最终组装------graph.py 里那六行代码的秘密
System Prompt 的最终组装发生在 create_deep_agent 的末尾,一共就六行代码:
python
# 第一行:Profile 叠加到 BASE 上
base_prompt = _apply_profile_prompt(_profile, BASE_AGENT_PROMPT)
# 第二行:如果没有 USER,直接用 base_prompt
if system_prompt is None:
final_system_prompt = base_prompt
# 第三行:如果 USER 是 SystemMessage(带 cache_control),合并为多 content block
elif isinstance(system_prompt, SystemMessage):
final_system_prompt = SystemMessage(
content_blocks=[
*system_prompt.content_blocks, # USER 的块
{"type": "text", "text": f"\n\n{base_prompt}"} # SDK 的块
]
)
# 第四行:如果 USER 是普通字符串,简单拼接
else:
final_system_prompt = system_prompt + "\n\n" + base_prompt
等等,这不是四层吗?怎么只有 USER + (BASE/CUSTOM) + SUFFIX?
因为 _apply_profile_prompt 已经把 CUSTOM 和 SUFFIX 处理好了------它返回的 base_prompt 实际上是 (BASE 或 CUSTOM) + SUFFIX 的结果。所以最终组装看到的 base_prompt 已经是"三合一"了。
完整的四层展开:
swift
final_system_prompt =
USER + "\n\n" + ← 你传的 system_prompt
(CUSTOM or BASE) + "\n\n" + ← _apply_profile_prompt 返回
SUFFIX ← 已经包含在 base_prompt 里了
但是!还有第五层------Memory。
🧠 第五幕:Memory------那个最贵的餐后甜点
Memory 不是在 create_deep_agent 里组装的,而是在运行时 由 MemoryMiddleware 动态注入的。它坐的位置比 SUFFIX 还靠后------紧贴着对话历史。
Memory 注入的时机
xml
System Prompt 组装流程(运行时):
┌──────────────────────────────┐
│ USER (system_prompt 参数) │ ← create_deep_agent 时确定
├──────────────────────────────┤
│ BASE / CUSTOM │ ← create_deep_agent 时确定
├──────────────────────────────┤
│ SUFFIX │ ← create_deep_agent 时确定
├──────────────────────────────┤ ── AnthropicPromptCaching 的
│ │ 缓存边界在这里!
│ <agent_memory> │ ← 每次请求动态注入
│ 路径1: 内容... │
│ 路径2: 内容... │
│ </agent_memory> │
│ │
│ <memory_guidelines> │ ← 记忆管理指南
│ 何时更新记忆... │
│ 何时不更新记忆... │
│ </memory_guidelines> │
└──────────────────────────────┘
为什么 Memory 要放在缓存边界之后?
这是一个极其精妙的设计。看 MemoryMiddleware 的初始化参数:
python
MemoryMiddleware(
backend=backend,
sources=memory,
add_cache_control=True, # 👈 关键参数
)
当 add_cache_control=True 且模型是 Anthropic 时,MemoryMiddleware 会在 <memory_guidelines> 的最后一个 content block 上加一个标记:
python
blocks[-1] = {**base, "cache_control": {"type": "ephemeral"}}
这个标记告诉 Anthropic API:从这里开始是可变内容,前面的系统提示词请缓存起来。
为什么?因为 Memory 是动态的 ------用户每次对话都可能更新 AGENTS.md,导致 Memory 内容变化。如果把 Memory 放在缓存边界之前,每次 Memory 变化都会让整个系统提示词的缓存失效------那可是几 K tokens 的缓存啊,每个请求重新算一遍,钱包直接出血。
而把 Memory 放在缓存边界之后:
- 前面的 USER + BASE/CUSTOM + SUFFIX 缓存住,跨请求复用
- Memory 部分独立变化,只影响后面的 token 计算
Memory 的注入方式
MemoryMiddleware 不是简单的字符串拼接------它用的是 append_to_system_message,这个函数会把 Memory 文本作为新的 content block 追加到 SystemMessage 的 content_blocks 列表里:
python
def append_to_system_message(
system_message: SystemMessage | None,
text: str,
) -> SystemMessage:
new_content = list(system_message.content_blocks) if system_message else []
if new_content:
text = f"\n\n{text}"
new_content.append({"type": "text", "text": text})
return SystemMessage(content_blocks=new_content)
这意味着每个"层"都是一个独立的 content block------Anthropic 的 Prompt Caching 是按 content block 粒度做缓存的,不是按整个文本。所以这个设计不是巧合,是刻意为之。
Memory 加载了什么?
MemoryMiddleware 在 before_agent 阶段从 backend 下载 AGENTS.md 文件,然后在 modify_request 阶段注入到 system prompt。加载的内容被包裹在 <agent_memory> 标签里,后面跟着 <memory_guidelines>------一份详细的"何时更新记忆、何时不更新"的行为指南。
这份指南很长(大概 2000 tokens),包含 6 个"何时更新"规则、6 个"何时不更新"规则、3 个示例。它的核心原则:
- 用户说"记住这个"→ 立即更新
- 用户纠正你的行为 → 立即更新(捕捉背后的原则,不只是表面错误)
- 临时信息("我今晚打篮球")→ 不更新
- 一次性任务("帮我找个菜谱")→ 不更新
🔥 如果面试官问你:Memory 和普通 system_prompt 有什么区别?
答:三个维度:
- 时机 :
system_prompt在create_deep_agent时确定,之后不变;Memory 在每次请求前动态加载,可以跨请求变化。 - 缓存 :
system_prompt在缓存边界之前,跨请求复用缓存;Memory 在缓存边界之后,独立失效。 - 语义 :
system_prompt是"行为指令"(你该怎么做事);Memory 是"知识积累"(你之前学到了什么)。
🔄 第六幕:子智能体的 Prompt------另一个世界
别忘了,子智能体也有 System Prompt,而且它的组装路径和主智能体不完全一样。
GP 子智能体的 Prompt 组装
自动创建的 General-Purpose 子智能体有两套逻辑:
python
if gp_profile.system_prompt is not None:
# 路径 A:GP 专属 system_prompt 优先
gp_prompt = gp_profile.system_prompt
if _profile.system_prompt_suffix is not None:
gp_prompt = gp_prompt + "\n\n" + _profile.system_prompt_suffix
general_purpose_spec["system_prompt"] = gp_prompt
else:
# 路径 B:走标准的 _apply_profile_prompt
general_purpose_spec["system_prompt"] = _apply_profile_prompt(
_profile, GENERAL_PURPOSE_SUBAGENT["system_prompt"]
)
路径 A :如果 Profile 同时设了 gp_profile.system_prompt 和 profile.base_system_prompt,GP 专属的赢 ------因为 GP 专属配置比全局配置更具体。但 system_prompt_suffix 仍然会叠加在最上面。
路径 B :如果没有 GP 专属配置,就用标准的 _apply_profile_prompt,传入 GP 的默认 prompt 作为 base_prompt(注意,这里 BASE 不是 BASE_AGENT_PROMPT,而是 GP 自己的默认 prompt)。
声明式子智能体的 Prompt 组装
用户自定义的 SubAgent 走的是更简单的路径:
python
processed_spec["system_prompt"] = _apply_profile_prompt(
_subagent_profile, spec["system_prompt"]
)
这里 spec["system_prompt"] 是用户在 SubAgent 定义里写的 prompt,它代替了 BASE_AGENT_PROMPT 作为 base_prompt 参数。Profile 的 base_system_prompt 和 system_prompt_suffix 照常叠加。
所以 :子智能体的 Prompt 三层是 (用户写的 prompt 或 CUSTOM) + SUFFIX------没有 BASE_AGENT_PROMPT 的参与,也没有主智能体 system_prompt 参数的参与。子智能体是独立的个体,有自己的"出厂设定"。
🗺️ 全景图:一次请求的 System Prompt 完整生命周期
sql
┌─────────────────────────────────────────────────────────┐
│ create_deep_agent() --- 构建时 │
│ │
│ 1. resolve_model(model) │
│ └→ 返回 model 实例 │
│ │
│ 2. _harness_profile_for_model(model, spec) │
│ ├→ 精确匹配 "anthropic:claude-sonnet-4-6" │
│ ├→ Provider 匹配 "anthropic" │
│ └→ 合并(如两者都存在) │
│ │
│ 3. _apply_profile_prompt(_profile, BASE_AGENT_PROMPT) │
│ ├→ CUSTOM 替换 BASE(如果设了) │
│ └→ SUFFIX 追加(如果设了) │
│ │
│ 4. 拼接 USER + base_prompt │
│ ├→ system_prompt=None → 直接用 base_prompt │
│ ├→ system_prompt=str → USER + "\n\n" + base_prompt │
│ └→ system_prompt=SystemMessage → 合并 content_blocks│
│ │
│ ── 静态 System Prompt 到此完成 ── │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 每次请求 --- 运行时 │
│ │
│ 5. MemoryMiddleware.before_agent() │
│ └→ 从 backend 下载 AGENTS.md 文件 │
│ │
│ 6. MemoryMiddleware.modify_request() │
│ ├→ 格式化 <agent_memory> 标签 │
│ ├→ append_to_system_message() 追加到末尾 │
│ └→ 如果是 Anthropic → 加 cache_control 标记 │
│ │
│ ── 完整 System Prompt 到此完成 ── │
│ │
│ 最终发送给模型的内容: │
│ ┌──────────────────────┐ │
│ │ USER │ ← 缓存边界前 │
│ │ BASE / CUSTOM │ │
│ │ SUFFIX │ │
│ ├──────────────────────┤ ← AnthropicPromptCaching │
│ │ <agent_memory> │ ← 缓存边界后 │
│ │ <memory_guidelines> │ │
│ └──────────────────────┘ │
└─────────────────────────────────────────────────────────┘
💡 三个值得记住的设计
1. "最贵的位置给最重要的信息"
SUFFIX 坐在 Prompt 的最末尾,紧挨着对话历史------这是因为 Transformer 的注意力机制在序列两端最高(primacy + recency effect)。把模型调优指导放在最末尾,就是让模型每次回复时最先"想起"这些规则。
这不是玄学,这是有论文支撑的------"Lost in the Middle"(Liu et al., 2023)证明,LLM 对序列中间的信息注意力最低。Deep Agents 的四层顺序(USER → BASE → SUFFIX)就是把最重要的东西放在首尾两端。
2. "缓存边界是金钱边界"
Anthropic 的 Prompt Caching 是按 token 计费的------缓存命中的 token 价格是正常价格的 10%。Memory 放在缓存边界之后,意味着:
- 如果 Memory 没变 → 缓存命中,省钱
- 如果 Memory 变了 → 只有 Memory 部分重新计算,前面的部分仍然命中
如果把 Memory 放在缓存边界之前呢?每次 Memory 变化,整个系统提示词的缓存全部失效 。假设系统提示词 3000 tokens,Memory 500 tokens,那就是 3000 tokens 的缓存从 10% 变成 100%,每个请求多花 2700 tokens 的钱。
按 Claude Sonnet 的定价( <math xmlns="http://www.w3.org/1998/Math/MathML"> 3 / M i n p u t ),每 1000 次请求多花 3/M input),每 1000 次请求多花 </math>3/Minput),每1000次请求多花8.1。一年下来就是一辆自行车的钱。
3. "子智能体是独立公民,不是主智能体的附庸"
子智能体的 System Prompt 不继承主智能体的 system_prompt 参数。它有自己的 base prompt(用户在 SubAgent 里写的),自己的 Profile 叠加,自己的 SUFFIX。这保证了:
- 主智能体说"你是客服"→ 不会污染子智能体说"你也是客服"
- 子智能体可以是完全不同的角色(一个查代码,一个写文档,一个跑测试)
- Profile 的 SUFFIX 仍然统一叠加------因为模型调优是跨角色的(不管你是什么角色,并行工具调用、先查后答这些规则都适用)
🔧 实战:如何自定义 System Prompt?
场景 1:最简单的------加一段自定义指令
python
agent = create_deep_agent(
model="anthropic:claude-sonnet-4-6",
system_prompt="你是一个专业的代码审查员。只关注安全漏洞和性能问题。",
)
最终 Prompt 顺序:你的指令 → BASE_AGENT_PROMPT → Claude SUFFIX
场景 2:替换整个 BASE
python
from deepagents import HarnessProfile, register_harness_profile
register_harness_profile(
"anthropic:claude-sonnet-4-6",
HarnessProfile(
base_system_prompt="你是一个只回答代码问题的助手。不要回答任何非代码问题。",
),
)
最终 Prompt 顺序:USER → 你的 CUSTOM → Claude SUFFIX
⚠️ 注意:CUSTOM 替换了 BASE,但 SUFFIX 仍然会叠加。如果你想完全控制,SUFFIX 也得设成 None(但那是合并后的 Profile 行为,如果之前已经注册了 SUFFIX,合并后 Override 优先,你得显式设个空字符串......算了,这个边缘场景一般人不会碰到)。
场景 3:加 Memory
python
agent = create_deep_agent(
model="anthropic:claude-sonnet-4-6",
memory=["./AGENTS.md", "./.deepagents/AGENTS.md"],
)
最终 Prompt 顺序:BASE_AGENT_PROMPT → Claude SUFFIX → <agent_memory> → <memory_guidelines>
场景 4:带 cache_control 的 SystemMessage
python
from langchain_core.messages import SystemMessage
agent = create_deep_agent(
model="anthropic:claude-sonnet-4-6",
system_prompt=SystemMessage(
content_blocks=[
{"type": "text", "text": "你是一个代码审查员。", "cache_control": {"type": "ephemeral"}},
]
),
)
最终结果:SystemMessage 的 content_blocks 会变成:
python
[
{"type": "text", "text": "你是一个代码审查员。", "cache_control": {"type": "ephemeral"}},
{"type": "text", "text": "\n\n<BASE_AGENT_PROMPT>\n\n<Claude SUFFIX>"},
]
cache_control 标记被保留,SDK 的内容作为新的 content block 追加。
🎯 总结
| 概念 | 一句话 | 源码位置 |
|---|---|---|
| BASE_AGENT_PROMPT | SDK 的默认行为规范 | graph.py |
| _apply_profile_prompt | CUSTOM 替换 BASE + SUFFIX 追加 | harness_profiles.py |
| HarnessProfile | 模型的人格面具(SUFFIX 为主) | harness_profiles.py |
| Profile 合并 | 三级回退 + 字段级 merge | harness_profiles.py |
| 内置 SUFFIX | Claude 三件套 / Opus 加餐 / Codex 完全不同 | profiles/harness/_*.py |
| MemoryMiddleware | 动态注入 + cache_control 隔离 | middleware/memory.py |
| append_to_system_message | 按 content block 追加 | middleware/_utils.py |
| 最终组装 | 六行代码,四层(USER → BASE/CUSTOM → SUFFIX → Memory) | graph.py |
四层不变量:
- USER 永远在最前面(调用者意志 > SDK 默认)
- SUFFIX 永远在 BASE/CUSTOM 后面(模型调优 > 通用规范)
- Memory 永远在缓存边界后(动态内容不要污染静态缓存)
- 子智能体不继承主智能体的 USER(独立公民原则)
🔮 下篇预告
手撕 LangChain Deep Agents 源码(三):中间件链------12 层中间件是怎么串成一条执行链的
下一篇我们将深入中间件的执行机制:
AgentMiddleware的四个钩子:before_agent/wrap_model_call/after_agent/on_tool_call- 中间件链的"洋葱模型"------请求从外到内、响应从内到外
PatchToolCallsMiddleware的暴力美学------为什么需要"补丁"中间件SummarizationMiddleware的精巧------对话太长怎么办HumanInTheLoopMiddleware的打断------人机协作的最后一道防线
预告问题 :如果中间件链是洋葱模型,那 before_agent 和 wrap_model_call 有什么区别?为什么需要两个钩子?
(提示:一个是"改状态",一个是"改请求"------但边界比你想的更模糊。)
"好的约束不是枷锁,是护栏------四层叠加的每一层,都是在回答一个问题:'谁来决定模型的行为?'USER 说你听我的,BASE 说你是个靠谱的助手,SUFFIX 说你作为 Claude/Codex 要注意这些,Memory 说我之前学到了这些。四个声音,一个方向。"