s10:系统提示 — 带缓存的运行时提示组装

核心思想


系统提示词(System Prompt)不再是一个写死的巨型字符串,而是在运行时根据 Agent 当前真实状态动态组装的产物,并通过确定性序列化缓存避免重复计算。本章分为三个递进主题:

  1. 提示词模块化:把提示词拆成可复用片段
  2. 上下文感知的动态组装:根据真实状态按需拼接
  3. 基于确定性序列化的缓存:避免反复拼接的性能浪费

1. 提示词模块化


  • 概念:系统提示词被分解成一些独立、可复用的片段,存储在 PROMPT_SECTIONS 字典里。每个片段对应一个主题,如"身份"、“可用工具”、“工作区”、“记忆”。
  • 优势:这让提示词的管理、修改和组合变得极其灵活。未来可以很容易地按需添加、移除或替换提示词的某个部分,而不影响其他部分。

2. 上下文感知的动态组装


  • 概念assemble_system_prompt(context) 函数是组装过程的"大脑"。它接收一个描述当前状态的 context 字典,然后基于实际状态,有条件地选择并拼接提示片段。
  • 判断是"状态驱动"的,而非"规则驱动"的:例如,它不会去猜有没有记忆,而是直接检查 contextmemories 字段是否为空。这保证了系统提示词永远是 Agent 当前所处真实环境的精确反映。

3. 基于确定性序列化的缓存


get_system_prompt(context) 函数封装了一个缓存层。只要 context 的内容没变,系统提示词就无需重新拼接,直接返回上次的字符串。

它使用 json.dumpscontext 字典序列化为一个字符串作为缓存的键(key)。这是一种确定性序列化,因为 Python 内置的 hash() 函数对于复杂对象不稳定,且自带随机化,不能作为持久化或跨进程的缓存键。json.dumps 保证了相同内容的字典总是生成相同的字符串。

为什么不用 hash() Python 的 hash() 对字符串/字典会引入随机盐(hash randomization),同一次进程内相同输入得到相同输出,但跨进程/跨重启结果不同。缓存键必须稳定,所以必须用确定性的 json.dumps

这在 Agent 多轮工具调用循环中非常有用。每一次 while 循环可能都会重新评估上下文,但大多数情况下上下文(如工具列表、工作区)并未改变。缓存避免了不必要的字符串拼接开销。


静态资源:prompt 片段定义


这种设计将**"提示词内容"与"组装逻辑"彻底分离**。修改提示词内容只需要修改这个字典,而不需要修改复杂的 assemble_system_prompt 函数。

# 组装 system prompt 的基础资源池。在更复杂的系统中,这里可能会包含关于安全规则、
# 代码风格、响应格式、角色扮演等几十个不同的片段。
PROMPT_SECTIONS = {
    "identity": "You are a coding agent. Act, don't explain.",
}

核心逻辑:动态组装函数


实现了基于状态的、有条件的提示词拼接逻辑。

  • 上下文协议context 是一个描述 Agent 当前运行环境状态的字典。它定义了一个约定:context 必须包含 enabled_toolsworkspacememories 这些字段。任何想要影响系统提示词的模块,都必须通过修改 context 来实现,而不是直接修改提示词字符串。
context = {
    "enabled_tools": ["read_file", "write_file", "search"],
    "workspace": "/home/user/project",
    "memories": "User prefers concise answers",
    "current_time": "2024-01-15T10:30:00",
    "user_preferences": {"language": "Chinese"}
}
  • 声明式组装assemble_system_prompt 函数本身是一个声明式的描述,它清晰地列出了组装规则:“总是加载身份”、“如果有工具列表,就添加工具描述”、“如果工作区存在,就添加”、“如果有记忆,就添加”。这种代码读起来就像一份说明书。
def assemble_system_prompt(context: dict) -> str:
    """基于当前上下文选择并拼接提示片段。"""
    sections = []
    # 始终加载 —— 身份
    sections.append(PROMPT_SECTIONS["identity"])
    # 动态部分 —— 来自上下文的工具
    tools = ", ".join(context.get("enabled_tools", []))
    if tools:
        sections.append(f"Available tools: {tools}.")
    # 工作区
    sections.append(f"Working directory: {context.get('workspace', WORKDIR)}")
    # 条件加载 —— 仅当存在有效记忆内容时加载
    memories = context.get("memories", "")
    if memories:
        sections.append(f"Relevant memories:\n{memories}")
    return "\n\n".join(sections)

性能优化:缓存包装器


进程内缓存实现,缓存保护了 assemble_system_prompt 这个"昂贵"的操作。虽然字符串拼接看起来很快,但当提示词片段变得非常多、且被高频调用时(如 Agent 的每一轮思考和工具调用后),这种优化就开始显现价值。

# 模块级全局变量充当缓存存储,缓存只在当前进程生命周期内有效。
_last_context_key = None
_last_prompt = None

def get_system_prompt(context: dict) -> str:
    """缓存包装器 —— 仅在上下文发生变化时重新组装。"""
    # 在函数内部重新赋值时需要 global
    global _last_context_key, _last_prompt
    # 1. 生成确定性上下文键值
    # 把 context 字典序列化为一个确定性的 json 字符串,三个参数保证稳定性
    #   sort_keys=True:按 key 字母顺序输出
    #   ensure_ascii=False:保留中文等非 ASCII 字符原样
    #   default=str:不可序列化对象兜底转为字符串
    key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)
    # 2. 缓存命中检查
    if key == _last_context_key and _last_prompt:
        print("  \033[90m[cache hit] system prompt unchanged\033[0m")
        return _last_prompt
    # 3. 缓存未命中,更新缓存并返回新提示
    _last_context_key = key
    _last_prompt = assemble_system_prompt(context)
    # 4. 日志记录
    # ...
    return _last_prompt

集成:在上下文更新与 Agent 循环中


  • update_context感知(Sense)阶段:它通过检查文件系统等,来更新 Agent 对自己所处环境的认知(即 context 字典——enabled_toolsworkspacememories)。
  • get_system_prompt决策/响应(Act/Respond)阶段的一部分:它基于最新的认知,进行进程内缓存。
  • 循环内update_contextget_system_prompt 被放在了 while True 循环的内部,在每次工具调用后执行。这意味着,如果 Agent 运行了一个脚本导致工作目录改变,或者它自己写入了新的记忆文件,那么下一轮与 LLM 通信时,system 提示词就能立即反映这些新变化。Agent 的**"世界观"是实时更新的**。
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,
    }

def agent_loop(messages: list, context: dict):
    """主循环 —— 使用组装后的系统提示词,而非硬编码的 SYSTEM。"""
    system = get_system_prompt(context)
    while True:
        # 发出模型推理调用 处理单个用户请求的完整执行过程
        response = client.messages.create(
            model=MODEL, system=system, messages=messages,
            tools=TOOLS, max_tokens=8000)
        # ... (追加助手消息、处理工具调用) ...
        # 关键:每轮工具调用后,重新评估上下文和系统提示词
        # 这里 update 主要是更新 memory
        context = update_context(context, messages)
        system = get_system_prompt(context)

update_context 的调用地点

if __name__ == "__main__":
    history = []
    # 🔴 第一次调用:初始化上下文
    context = update_context({}, [])  # 传入空字典和空列表,建立初始认知
    # 处理多个独立的用户请求,以及请求之间的状态管理
    while True:
        query = input(...)
        history.append({"role": "user", "content": query})
        # 进入 agent_loop,内部会反复调用 update_context
        agent_loop(history, context)
        # 🔴 第二次调用:agent_loop 结束后更新上下文
        # 这里工作区可能变化、agent 解锁新工具、完成整个任务后可能提取多条记忆
        context = update_context(context, history)  # 为什么要更新?
        # 打印回复...

本模块知识点


  • 提示词模块化:PROMPT_SECTIONS 字典存独立可复用片段。
  • 上下文感知组装assemble_system_prompt(context) 基于真实状态有条件拼接(状态驱动,非规则驱动)。
  • 确定性序列化缓存get_system_promptjson.dumps(sort_keys, ensure_ascii=False) 作缓存键,避免不稳定 hash()
  • 集成:update_context 每轮工具调用后更新认知,system 提示词实时反映变化。