s10:系统提示 — 带缓存的运行时提示组装
s10:系统提示 — 带缓存的运行时提示组装
核心思想
系统提示词(System Prompt)不再是一个写死的巨型字符串,而是在运行时根据 Agent 当前真实状态动态组装的产物,并通过确定性序列化缓存避免重复计算。本章分为三个递进主题:
- 提示词模块化:把提示词拆成可复用片段
- 上下文感知的动态组装:根据真实状态按需拼接
- 基于确定性序列化的缓存:避免反复拼接的性能浪费
1. 提示词模块化
- 概念:系统提示词被分解成一些独立、可复用的片段,存储在
PROMPT_SECTIONS字典里。每个片段对应一个主题,如"身份"、“可用工具”、“工作区”、“记忆”。 - 优势:这让提示词的管理、修改和组合变得极其灵活。未来可以很容易地按需添加、移除或替换提示词的某个部分,而不影响其他部分。
2. 上下文感知的动态组装
- 概念:
assemble_system_prompt(context)函数是组装过程的"大脑"。它接收一个描述当前状态的context字典,然后基于实际状态,有条件地选择并拼接提示片段。 - 判断是"状态驱动"的,而非"规则驱动"的:例如,它不会去猜有没有记忆,而是直接检查
context中memories字段是否为空。这保证了系统提示词永远是 Agent 当前所处真实环境的精确反映。
3. 基于确定性序列化的缓存
get_system_prompt(context) 函数封装了一个缓存层。只要 context 的内容没变,系统提示词就无需重新拼接,直接返回上次的字符串。
它使用 json.dumps 将 context 字典序列化为一个字符串作为缓存的键(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_tools、workspace和memories这些字段。任何想要影响系统提示词的模块,都必须通过修改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_tools、workspace、memories)。get_system_prompt是决策/响应(Act/Respond)阶段的一部分:它基于最新的认知,进行进程内缓存。- 循环内:
update_context和get_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_prompt用json.dumps(sort_keys, ensure_ascii=False)作缓存键,避免不稳定hash()。 - 集成:
update_context每轮工具调用后更新认知,system 提示词实时反映变化。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 茯茶养生人的博客!










