说明:💡同行为发给 AI 的用户提示词问题
目录
- Prompt
-
- [系统提示词 vs 用户提示词的分工设计](#系统提示词 vs 用户提示词的分工设计)
-
- [1. 缓存效率(最关键的工程原因)](#1. 缓存效率(最关键的工程原因))
- [2. 语义角色对齐](#2. 语义角色对齐)
- [3. 会话生命周期差异](#3. 会话生命周期差异)
- [1. API 协议的语义要求](#1. API 协议的语义要求)
- [2. 项目正在把 MCP 指令移出系统提示词](#2. 项目正在把 MCP 指令移出系统提示词)
- [3. 缓存降级策略](#3. 缓存降级策略)
Prompt
💡检索所有对于传入给模型接口的 prompt 使用的是纯文本,还是使用了类 xml 格式?使用了 xml 格式的有哪些,以及为什么要这样使用?
XML格式使用:结构化区分消息的"类型"和"内容"
- 防止混淆 --- 用户消息里混杂了用户输入、终端输出、系统注入、任务通知等多种来源。XML 标签让模型能明确区分"这是 bash 输出"还是"用户在说话"。例如
<bash-stdout>vs 裸文本的区别。 - 可解析性 --- 代码通过
extractTag()函数从历史消息中反向提取标签内容,用于渲染和逻辑判断(如判断一条消息是否是 bash 命令)。 - 与模型的认知对齐 --- Claude 本身在训练中就大量接触 XML 标签格式的 prompt,用 XML 包裹内容能让模型更准确地理解边界。
<system-reminder>就是一个典型例子------告诉模型"这段是上下文,可能相关也可能无关"。 - 嵌套结构表达 --- 任务通知 (
<task-notification>内含<task-id>,<summary>等子标签) 需要表达层级关系,XML 天然适合。 - 安全转义 --- 动态内容通过
escapeXml()转义后放入标签,避免输出中的</>破坏结构完整性。
总结:系统提示词用纯文本(因为只需线性拼接),用户消息中用 XML 标签(因为需要区分多种消息来源,且需要被模型和代码双向解析)。
typescript
export function extractTag(html: string, tagName: string): string | null {
if (!html.trim() || !tagName.trim()) {
return null
}
const escapedTag = escapeRegExp(tagName)
// Create regex pattern that handles:
// 1. Self-closing tags
// 2. Tags with attributes
// 3. Nested tags of the same type
// 4. Multiline content
const pattern = new RegExp(
`<${escapedTag}(?:\\s+[^>]*)?>` + // Opening tag with optional attributes
'([\\s\\S]*?)' + // Content (non-greedy match)
`<\\/${escapedTag}>`, // Closing tag
'gi',
)
let match
let depth = 0
let lastIndex = 0
const openingTag = new RegExp(`<${escapedTag}(?:\\s+[^>]*?)?>`, 'gi')
const closingTag = new RegExp(`<\\/${escapedTag}>`, 'gi')
while ((match = pattern.exec(html)) !== null) {
// Check for nested tags
const content = match[1]
const beforeMatch = html.slice(lastIndex, match.index)
// Reset depth counter
depth = 0
// Count opening tags before this match
openingTag.lastIndex = 0
while (openingTag.exec(beforeMatch) !== null) {
depth++
}
// Count closing tags before this match
closingTag.lastIndex = 0
while (closingTag.exec(beforeMatch) !== null) {
depth--
}
// Only include content if we're at the correct nesting level
if (depth === 0 && content) {
return content
}
lastIndex = match.index + match[0].length
}
return null
}
系统提示词 vs 用户提示词的分工设计
💡为什么该项目这样考虑哪些内容放系统提示词,哪些内容放用户提示词?
bash
System Prompt (system 参数)
├─ 归属头 (attribution header) ← 不缓存
├─ CLI 前缀 "You are Claude Code..." ← org 级缓存
├─ 静态指令 (工具使用规范、行为准则等) ← global 级缓存
├─ ── DYNAMIC_BOUNDARY ── ← 分界线
└─ 动态内容 (MCP 工具说明等) ← 不缓存
User Message (messages 参数,前置注入)
└─ <system-reminder> 包裹:
├─ claudeMd (CLAUDE.md 内容)
├─ currentDate (当前日期)
└─ "可能相关也可能无关" 免责声明
核心设计原则:
1. 缓存效率(最关键的工程原因)
系统提示词支持 Anthropic API 的 prompt caching,有三级缓存 scope:
| Scope | 含义 | 缓存命中率 |
|---|---|---|
global |
跨组织共享 | 最高(所有用户共享) |
org |
组织内共享 | 中等 |
| 无 | 不缓存 | 最低(每次请求重新计算 token) |
- 静态指令 (工具规范、行为准则 ~50-70K tokens)放在系统提示词的 boundary (
DYNAMIC_BOUNDARY)之前 ,使用global缓存 --- 所有 Claude Code 用户共享一份缓存,成本近乎为零 - 动态内容(MCP 工具描述等,每个用户不同)放在 boundary 之后,不缓存
- 归属头(包含用户指纹)不缓存
边界标记 prompts.ts 的注释写得很明确:
shell
* Boundary marker separating static (cross-org cacheable) content from dynamic content.
* Everything BEFORE this marker in the system prompt array can use scope: 'global'.
* Everything AFTER contains user/session-specific content and should not be cached.
* 边界标记:用于区分静态内容(可跨组织缓存)与动态内容。
* 系统提示词数组中,此标记之前的所有内容均可使用 global 作用域。
* 此标记之后的所有内容包含用户 / 会话专属信息,不应被缓存。
splitSysPromptPrefix 会根据这个 marker 将系统提示词拆分成不同缓存级别的块。
如果把 CLAUDE.md 放进系统提示词,每个用户的 CLAUDE.md 内容不同 → 系统提示词就变成了 per-user → global 缓存彻底失效 → 成本暴增。
2. 语义角色对齐
从 Claude 模型的角度:
- System = "你是什么角色、你怎么工作" --- 指令性的、稳定的
- User = "当前情况是什么、用户要什么" --- 情境性的、变化的
shell
系统提示词里放: 用户消息里放:
├─ 你是 Claude Code ├─ CLAUDE.md(项目自定义指令)
├─ 如何使用工具 ├─ 当前日期
├─ 行为准则和约束 ├─ git status 快照
├─ 输出格式要求 └─ <system-reminder> 免责声明
└─ 全局规范(对所有用户一致)
CLAUDE.md 本质上是用户提供的项目上下文 ,不是 Claude Code 的固有行为规范。把它放在 user message 里并用 <system-reminder> 包裹,同时加上 "this context may or may not be relevant" 的免责声明,让模型知道这是辅助参考 而非强制指令。这降低了 CLAUDE.md 内容与系统指令冲突时的优先级问题。
3. 会话生命周期差异
- 系统提示词 在 compaction(上下文压缩)后完整保留 ,因为它作为 API 的
system参数独立于消息历史 - 用户消息 在 compaction 时会被摘要压缩
这意味着:
- git status 这种快照型信息 ,放系统提示词里反而不好 --- compaction 后它不会更新,但对话中已经执行了 git 操作,模型看到的 status 是过时的。不过目前项目把它放在了系统提示词里(
appendSystemContext),这是一个务实选择 --- git status 虽然会过时,但系统提示词有缓存优势 - CLAUDE.md 放用户消息里 --- compaction 后会自然被摘要掉,但因为 CLAUDE.md 在每轮对话都通过
prependUserContext重新注入,所以不会丢失
总结:系统提示词放"对所有用户都一样"的静态指令以最大化 prompt caching 收益;用户消息放"因用户/会话而异"的动态上下文。这是缓存效率、语义正确性、会话生命周期三者的平衡。
💡为什么调用模型API,需要说明这是哪类缓存?
不是在告诉模型,是在控制发给 API 的 cache_control 字段。整个流程是:
shell
代码逻辑(客户端) API 请求(发到 Anthropic 服务端)
splitSysPromptPrefix()
├─ block.cacheScope = null → { text: "..." } // 无 cache_control
├─ block.cacheScope = 'org' → { text: "...", cache_control: { type: "ephemeral" } }
└─ block.cacheScope = 'global' → { text: "...", cache_control: { type: "ephemeral", scope: "global" } }
最终发给 API 的请求长这样(简化):
json
{
"system": [
{ "type": "text", "text": "You are Claude Code..." },
{ "type": "text", "text": "<静态工具规范...>", "cache_control": { "type": "ephemeral", "scope": "global" } },
{ "type": "text", "text": "<动态MCP内容...>" }
]
}
模型完全看不到 cache_control。这个字段是给 Anthropic 服务端的基础设施用的 --- 服务端根据它决定是否复用已缓存的 KV 状态,避免重复计算那 ~50K tokens 的 attention。
skipGlobalCacheForSystemPrompt 的触发条件是"当前有 MCP 工具存在"(claude.ts:1340-1345)。因为 MCP 工具描述是用户自定义的、放在系统提示词尾部,如果仍然标记 global 缓存,Anthropic 服务端会发现不同用户的全局缓存键不一致 → 缓存失效 → 白打了标记还浪费了一个 cache block 位。所以有 MCP 工具时降级为 org 级缓存。
💡以 MCP 工具为例,既然它是用户定义的,为什么要放在系统提示词尾部,而不是用户提示词(User Prompt)中?
MCP 指令放在系统提示词,而不是用户消息,原因是 Claude API 的工具调用机制。
1. API 协议的语义要求
Anthropic API 的设计中,tools 数组里的每个工具只有 name、description、input_schema 三个字段。但 MCP 服务器通常需要给模型额外的使用说明 --- 比如认证方式、调用注意事项、组合使用策略等,这些不适合塞在 description 的几百字里。
这些指令必须放在模型能稳定参考的位置。如果放在用户消息里:
- compaction 后可能被摘要掉 → 模型忘了 MCP 工具怎么用
- 每轮重新注入 → 但会被模型当成"新信息"而非"持久规则"
放在系统提示词里,模型把它当作持久行为规范对待,权重和稳定性都更高。
2. 项目正在把 MCP 指令移出系统提示词
看 prompts.ts 的注释:
typescript
// When delta enabled, instructions are announced via persisted
// mcp_instructions_delta attachments (attachments.ts) instead of this
// per-turn recompute, which busts the prompt cache on late MCP connect.
MCP 指令被标记为 DANGEROUS_uncachedSystemPromptSection(systemPromptSections.ts),意味着它每轮重新计算,会破坏缓存。
项目已经实现了 mcp_instructions_delta 机制(mcpInstructionsDelta.ts):当 MCP 服务器中途连接/断开时,增量指令作为 attachment(附件消息) 注入,而不是重写系统提示词。这样:
- 系统提示词保持不变 → 缓存不被破坏
- MCP 指令通过消息附件传递 → 模型仍然能看到
3. 缓存降级策略
当没有启用 delta 机制时,项目做了缓存降级 --- claude.ts:
typescript
// MCP tools are per-user → dynamic tool section → can't globally cache.
const needsToolBasedCacheMarker =
useGlobalCacheFeature &&
filteredTools.some(t => t.isMcp === true && !willDefer(t))
有 MCP 工具时,整个系统提示词从 global 缓存降级为 org 缓存,避免不同用户的 MCP 配置污染全局缓存。