在上一版本中,Agent 已具备记忆系统、上下文压缩、技能懒加载、权限管道及子 Agent 调度等完整能力栈。然而,每增加一项能力,system prompt 便冗长一分。最终形成的硬编码字符串不仅难以维护,更成为项目迭代的阻碍:
SYSTEM = (
f"You are a coding agent at {WORKDIR}. "
"Use tools to solve tasks. Act, don't explain. "
"Before starting any multi-step task, use todo_write. "
"Skills are available via list_skills and load_skill. "
"Relevant memories are injected below when available. "
# ... 更多段落
)
开发者往往不敢轻易修改这段代码:无法区分哪些是核心逻辑,哪些是调试残留;项目切换时需全量重写,却不知何处该留、何处该弃。这并非提示词撰写技巧的问题,而是架构设计的缺陷——将配置硬编码于业务逻辑中,无异于将数据库连接字符串写死在代码里。
三个真实的痛点
在探讨解决方案前,需明确硬编码 Prompt 带来的三个具体隐患:
1. 项目迁移成本高,边界模糊
当前 Agent 的 Prompt 混杂了工作目录、工具列表与行为规范。切换项目时,仅需变更部分参数(如目录),但硬编码字符串导致这些边界不可见。开发者被迫全量重写或进行高风险的字符串替换,无法复用身份定义等通用逻辑。
2. 局部修改引发全局副作用
新增工具描述可能与既有行为规范产生语义冲突。此类问题不会抛出异常,却会导致 Agent 行为异常,排查难度极大。LLM 对指令冲突的理解具有不可预测性,使得系统稳定性难以保障。
3. 无效信息稀释注意力并浪费 Token
即便无记忆文件,"Relevant memories..."等占位符仍随每次请求发送。System prompt 按轮次计费,无关指令不仅增加成本,更会分散 LLM 对核心任务的注意力,长期累积可能导致行为漂移。
解法:将 Prompt 转化为动态配置
核心改进在于模块化与动态组装:将硬编码字符串拆解为独立 Section,运行时依据真实状态按需加载。
系统划分为四个 Section,采用两种加载策略:
“始终加载”包含基础信息,“按需加载”则依赖文件系统或运行状态的真实反馈,而非简单的关键词匹配。
PROMPT_SECTIONS = {
"identity": "You are a coding agent. Act, don't explain.",
"tools": "Available tools: bash, read_file, write_file.",
"workspace": f"Working directory: {WORKDIR}",
"memory": "Relevant memories are injected below when available.",
}
此字典结构确保了各 Section 的独立性:修改工具列表不影响身份定义,新增模块无需理解整体逻辑。
三个核心函数的机制
assemble_system_prompt:动态组装策略
def assemble_system_prompt(context: dict) -> str:
sections = []
# 始终加载基础信息
sections.append(PROMPT_SECTIONS["identity"])
sections.append(PROMPT_SECTIONS["tools"])
sections.append(PROMPT_SECTIONS["workspace"])
# 按需加载 — 基于真实状态
memories = context.get("memories", "")
if memories:
sections.append(f"Relevant memories:\n{memories}")
return "\n\n".join(sections)
逻辑清晰:基础 Section 无条件加入,记忆 Section 仅在 context["memories"] 有值时注入实际内容。此举消除了无效占位符,既节省 Token 成本,又避免无关指令稀释 LLM 注意力。
get_system_prompt:进程内缓存优化
_last_context_key = None
_last_prompt = None
def get_system_prompt(context: dict) -> str:
global _last_context_key, _last_prompt
key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)
if key == _last_context_key and _last_prompt:
return _last_prompt
_last_context_key = key
_last_prompt = assemble_system_prompt(context)
return _last_prompt
同一轮对话中若 Context 未变,重复拼接字符串纯属浪费。此处采用缓存机制,关键细节在于使用 json.dumps 而非 hash() 生成 Key:
- 稳定性:Python 3.3+ 的
hash() 具有进程级随机化,且不支持 dict/list 类型,而 Context 正是嵌套结构。
- 一致性:
json.dumps 配合 sort_keys=True 确保相同内容的字典无论键序如何均产生唯一序列化结果。
需注意,此缓存仅解决进程内重复计算问题。相比之下,Claude Code 实现了 API 级别的静态/动态分离缓存,能进一步节省 Token 成本,这是后续优化的方向。
update_context:基于真实状态推导
def update_context(context: dict, messages: list) -> dict:
memories = ""
if MEMORY_INDEX.exists():
content = MEMORY_INDEX.read_text().strip()
if content:
memories = content
return {
"enabled_tools": list(TOOL_HANDLERS.keys()),
"workspace": str(WORKDIR),
"memories": memories,
}
Context 应如实反映运行态:
- 工具列表:直接读取注册表
TOOL_HANDLERS,新增工具自动生效,无需手动维护字符串。
- 记忆内容:检查文件系统
.memory/MEMORY.md 的存在性与内容,不依赖对话历史推测。
主循环集成与 Bug 修复
def agent_loop(messages: list, context: dict):
system = get_system_prompt(context)
while True:
response = client.chat.completions.create(
model=MODEL,
messages=[{"role": "system", "content": system}] + list(messages),
tools=TOOLS,
max_tokens=8000
)
messages.append(response.choices[0].message)
if not response.choices[0].message.tool_calls:
return
results = []
for tool_call in response.choices[0].message.tool_calls:
print(f"\033[36m> {tool_call.function.name}\033[0m")
handler = TOOL_HANDLERS.get(tool_call.function.name)
output = (
handler(**json.loads(tool_call.function.arguments))
if handler
elsef"Unknown: {tool_call.function.name}"
)
print(str(output)[:200])
results.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": str(output)
})
messages.append({"role": "user", "content": results})
# 每轮工具执行后重新评估上下文
context = update_context(context, messages)
system = get_system_prompt(context)
流程闭环:每轮循环获取 System Prompt,工具执行后更新 Context。若状态未变(如无新记忆),缓存命中直接返回;若状态变更,下一轮自动注入新内容。
Bug 修复说明:原始代码中存在连续两行 continue,导致工具执行逻辑被完全跳过。上述代码已移除此错误,确保循环体正常执行。
与 Claude Code 源码的差距分析
对比 Claude Code 源码(涉及 prompts.ts, systemPromptSections.ts 等文件),当前实现主要在以下三个维度存在差距:
1. Section 数量与动态性
当前版本固定为 4 个 Section。而 Claude Code 拥有十余个 Section,受 Feature Flag、输出风格、用户类型等多因素影响。其中 mcp_instructions 为易失性 Section,因 MCP Server 连接状态实时变化,无法缓存。
2. 多层缓存架构
Claude Code 实现了三层缓存:
- Lodash Memoize:会话级缓存,输入不变则不重算。
- Section 注册缓存:缓存动态 Section 计算结果,特定命令清除。
- API 级缓存:通过
splitSysPromptPrefix() 将 Prompt 分为静态块(Global Cache)与动态块。静态内容(如身份定义)不因动态内容变化而失效,显著降低长对话成本。
3. 静动分离的价值
SYSTEM_PROMPT_DYNAMIC_BOUNDARY 机制将恒定内容(Identity、规范)与变化内容(Token 预算、Git 状态)物理隔离。静态部分单独缓存,即使动态部分频繁变动,静态部分仍可命中缓存。对于 20-30KB 的静态 Prompt,这在长对话中意味着巨大的 Token 节省。当前版本虽未实现此机制,但架构已为此预留了扩展路径。
小结
本次重构核心在于将硬编码字符串转化为可维护、按需加载的配置对象。代码增量有限,却解决了迁移难、耦合高、成本浪费三大痛点。
下一步将聚焦于分布式系统的常态挑战:网络抖动、API 限流、上下文超限等错误恢复机制,包括 Token 预算升级、上下文压缩、指数退避及模型切换策略。