手搓Claude Code-第十章 system_prompt

手搓Claude Code-第十章 system_prompt

写在前面

  例如:还记得上一章末尾那个有关prompt的问题吗?紧接着本章就是在解决它。如果你写过最简单的 LLM Agent,你的system prompt大概长这样:

python 复制代码
SYSTEM = f"You are a coding agent at {os.getcwd()}. Use bash to solve tasks."

  但随着你给它加越来越多的能力------读文件、写文件、子 Agent、技能、记忆、压缩------这个字符串开始膨胀:

python 复制代码
SYSTEM = (
    f"You are a coding agent at {WORKDIR}.\n"
    f"Available skills:\n{catalog}\n"
    f"Available memories:\n{index}\n"
    f"Current todos:\n{todos}\n"
    f"Relevant memories:\n{injected}\n"
    "Respect user preferences from memory.\n"
    "When the user says 'remember', extract it as a memory.\n"
    # ... 还在继续加
)

  问题来了:这个字符串只在程序启动时拼一次,有的prompt我们真的需要吗?

  • 用户第一句话是"帮我格式化代码",你注入了"用户偏好 Tab 缩进"的记忆。但用户第二句话是"顺便读一下 README",跟缩进毫无关系------那段记忆还赖在 prompt 里占 token。
  • 用户中途加载了一个 skill,prompt 里没有这个 skill 的说明。
  • 用户做完了 todo 的前两项,prompt 里还显示"5 项待办全 pending"。

  其实本章的大部分代码在结局什么是硬编码什么是软编码的问题,而这中间的界限比较模糊 。这章的内容不多,但笔者却在这部分花费了相当时间去理解。详细完整代码见:

https://github.com/shareAI-lab/learn-claude-code/blob/main/s10_system_prompt/code.py

  本章,我们的任务是:

  1,了解prompt的动态注入,动手实现。

  2,跑几个任务测试。


一、了解prompt的动态注入,动手实现。

  动态 prompt 注入的思想很简单:不是在启动时拼一次就完事,而是每轮对话前后都重新评估一次上下文,再拼 prompt

  本章的核心在于新引入了一个context字典: context 这个字典------它是当前会话状态的快照:启用了哪些工具、加载了哪些记忆、当前有什么任务、用户说了什么偏好。每轮工具调用后,从最新的 messages 里推断新的 context,再重新拼 prompt。整个过程其实就是在维护这个context的过程。

python 复制代码
def agent_loop(messages: list, context: dict):
    # 首先拿到系统提示词
    system = get_system_prompt(context)
    while True:
        # 建立会话
        response = client.messages.create(
            model=MODEL, system=system, messages=messages,
            tools=TOOLS, max_tokens=8000)
        messages.append({"role": "assistant", "content": response.content})
        # 不是工具调用的response直接返回。(其他功能全部删去)
        if response.stop_reason != "tool_use":
            return

        # 是工具调用,对content中的block依次进行处理
        results = []
        for block in response.content:
            if block.type != "tool_use":
                continue
            print(f"\033[36m> {block.name}\033[0m")
            handler = TOOL_HANDLERS.get(block.name)
            output = handler(**block.input) if handler else f"Unknown: {block.name}"
            print(str(output)[:200])
            results.append({"type": "tool_result",
                            "tool_use_id": block.id, "content": output})
        messages.append({"role": "user", "content": results})

        # 处理完content,更新context,然后再拿一次系统提示词。
        context = update_context(context, messages)
        system = get_system_prompt(context)

  ok,接下来,我们就来一一实现agent_loop中的操作。首先是get_system_prompt() ,这个函数传入是为了传入context拿到动态注入的prompt,也就是_last_prompt。它其实也section。在该章节当中section由prompt_section和context构成。看到这里你可能还是不太理解,没关系继续往下看。

python 复制代码
# 存储上一次的上下文
_last_context_key = None
# 存储上次生成的提示词
_last_prompt = None

def get_system_prompt(context: dict) -> str:
    """接收context,返回一个str。"""
    global _last_context_key, _last_prompt
    """
    转化成json格式的字符串
    sort_keys=True:按 key 排序,保证相同内容产生相同字符串(顺序无关)
    ensure_ascii=False:保留非 ASCII 字符(如中文),不转义成 \uXXXX
    default=str:遇到无法序列化的对象(如自定义类),直接用 str() 转换,防止报错
    """
    key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)
    # 如果key和上次一样,并且lastprompt也存在就直接返回上次的提示词即可
    if key == _last_context_key and _last_prompt:
        print("  \033[90m[cache hit] system prompt unchanged\033[0m")
        return _last_prompt
    # 如果不一样,则动态注入
    _last_context_key = key
    _last_prompt = assemble_system_prompt(context)

    # 打印日志,是动态注入的内容,内容是固定的,只是为了反应有无memory的区别。
    loaded = ["identity", "tools", "workspace"]
    if context.get("memories"):
        loaded.append("memory")
    print(f"  \033[32m[assembled] sections: {', '.join(loaded)}\033[0m")
    return _last_prompt

  再来实现assemble_system_prompt(context),这个函数是传入当前的context,结合PROMPT_SECTIONS中的内容去生成对应的section,也就是最终拿到的prompt。

python 复制代码
def assemble_system_prompt(context: dict) -> str:
    """基于当前的context筛选并加载prompt"""
    sections = []

    # 先加载prompt_section(从PROMPT_SECTIONS中拿到)
    sections.append(PROMPT_SECTIONS["identity"]) # 目前是写死的,但只需要补充一个函数应该就可以更新

    # 再加载动态的工具(从context中拿到,下面三个都是)
    tools = ','.join(context.get("enabled_tools", []))
    if tools:
        sections.append(f"Available tools: {tools}.")

    # 再加载动态的工作区
    sections.append(f"Working directory: {context.get("workspace", WORKDIR)}")

    # 最后加载相关的memory
    memories = context.get("memories", "")
    if memories:
        sections.append(f"Relevant memories:\n{memories}")

    # 返回sections
    return "\n\n".join(sections)

  到现在,我们明白了,其实整个prompt = section = prompt_section + context。prompt_section和context都是动态的,所以我们还要写一个update_context(context: dict, messages: list),这个函数会根据上次的context和当前messages去更新当前的context。

python 复制代码
def update_context(context: dict, messages: list) -> dict:
    """传入当前context和messages,生成一个新的context"""
    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, # 会更新
    }

  再回到agent_loop部分。我们只需要注意两个节点,一个是刚开始,一个是结束时。

python 复制代码
def agent_loop(messages: list, context: dict):
    # 首先拿到系统提示词
    system = get_system_prompt(context)
    while True:
        # 建立会话
        response = client.messages.create(
            model=MODEL, system=system, messages=messages,
            tools=TOOLS, max_tokens=8000)
        messages.append({"role": "assistant", "content": response.content})
        # 不是工具调用的response直接返回。(其他功能全部删去)
        if response.stop_reason != "tool_use":
            return

        # 是工具调用,对content中的block依次进行处理
        results = []
        for block in response.content:
            if block.type != "tool_use":
                continue
            print(f"\033[36m> {block.name}\033[0m")
            handler = TOOL_HANDLERS.get(block.name)
            output = handler(**block.input) if handler else f"Unknown: {block.name}"
            print(str(output)[:200])
            results.append({"type": "tool_result",
                            "tool_use_id": block.id, "content": output})
        messages.append({"role": "user", "content": results})

        # 处理完content,更新context,然后再拿一次系统提示词。
        context = update_context(context, messages)
        system = get_system_prompt(context)

  到这里,就可以看出来,其实shareai给的这一章更像一种骨架或者示例,展示了一种动态注入的方式,但封装到最底层还是写死的。


二、跑几个测试

bash 复制代码
s10 >> read the file code.py in s10_system_prompt
  [assembled] sections: identity, tools, workspace, memory
> read_file
import os, subprocess, json
from pathlib import Path

try:
    import readline
    readline.parse_and_bind('set bind-tty-special-chars off')
except ImportError:
    pass

from anthropic import Anthrop
  [cache hit] system prompt unchanged
Here's the content of `s10_system_prompt/code.py`:

```python
import os, subprocess, json
from pathlib import Path

try:
    import readline
    readline.parse_and_bind('set bind-tty-special-chars off')
except ImportError:
    pass

from anthropic import Anthropic
from dotenv import load_dotenv

load_dotenv(override=True)
if os.getenv("ANTHROPIC_BASE_URL"):
    os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)

WORKDIR = Path.cwd()
...此处省略


This is an enhanced version of the coding agent that introduces **dynamic system prompt assembly** with caching. Key additions compared to earlier versions:

- **`PROMPT_SECTIONS`** --- a dict of static prompt sections (identity).
- **`assemble_system_prompt(context)`** --- builds the system prompt by combining identity, available tools, workspace path, and any relevant memories from the `.memory/MEMORY.md` file.
- **`get_system_prompt(context)`** --- caches the assembled prompt; returns the cached version if the context hasn't changed (avoids re-assembling on every API call).
- **`update_context(context, messages)`** --- rebuilds the context dict (tools, workspace, memories) after each agent loop iteration, so the system prompt can change dynamically (e.g., if new memories are added).

  只需要观察assembled sections: identity, tools, workspace, memory有没有出现。这些也是固定的,我在上面对应的代码中也标注出来了。


总结

  总结来说,这一章还是一种用于示范的骨架,后续章节应该还会针对这些没有完成的部分进行补充。

相关推荐
云端漫步19871 小时前
HarmonyOS NEXT AI 智能生活助手:AI 日程规划
人工智能·华为·生活·harmonyos
孙启超1 小时前
【AI应用开发】怎么降低 Agent 幻觉?有几种可行方案?
大数据·人工智能·llm·agent·rag·幻觉·ai应用开发
丘丘用户思思澪1 小时前
生产环境下优化 Agent Token 成本的系统化实战指南
人工智能
我是大卫1 小时前
图解RAG,一张图理解检索增强生成
人工智能
淡忘suuda1 小时前
生产级 AI Agent 评测体系实战:从数据集构建到自动回归与质量门禁
人工智能·数据挖掘·回归
spider_xcxc1 小时前
生产级AI智能体的可观测性实战:从指标监控到LLM评估
大数据·人工智能
高洁011 小时前
VLA:驱动具身智能迈向通用的关键引擎
人工智能·python·深度学习·transformer·知识图谱
数智化管理手记1 小时前
元数据和业务数据有什么区别?元数据价值体现在什么地方?
大数据·人工智能·数据挖掘·云计算
西安小哥1 小时前
AI时代技术工程师的"道法术器势":学习图谱、成长规划与角色重塑
人工智能·设计模式·程序员