大数跨境

如何给 Agent 的 System Prompt 做一次彻底的“去耦手术”?(Agent架构实操十)

如何给 Agent 的 System Prompt 做一次彻底的“去耦手术”?(Agent架构实操十) 智能体AI
2026-07-03
0
导读:深度拆解:为什么把 System Prompt 写死在代码里是严重的错误?
在上一版本中,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,采用两种加载策略:
Section
策略
内容
触发条件
identity
始终
角色定义与行为规范
无条件
tools
始终
可用工具列表
无条件
workspace
始终
工作目录路径
无条件
memory
按需
当前相关记忆内容
.memory/MEMORY.md 存在且有内容
“始终加载”包含基础信息,“按需加载”则依赖文件系统或运行状态的真实反馈,而非简单的关键词匹配。
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 预算升级、上下文压缩、指数退避及模型切换策略。
【声明】内容源于网络
0
0
智能体AI
1234
内容 467
粉丝 0
智能体AI 1234
总阅读13.3k
粉丝0
内容467