news 2026/9/29 21:43:01

learn-claude-code -s10 实战:用 system prompt 与 json.dumps 构建可缓存的 agent 配置

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
learn-claude-code -s10 实战:用 system prompt 与 json.dumps 构建可缓存的 agent 配置

1. 为什么你的 Agent 每轮都在重复拼同一段 system prompt

如果你正在跟着 learn-claude-code 的 s10 章节写 Agent,大概率已经踩过这个坑:system参数一开始是硬编码的字符串,写着写着工具变多了、记忆文件加进来了、工作目录也要动态注入,于是你开始用 f-string 拼字符串。跑起来没问题,但每轮对话都在重新拼一遍,哪怕这一轮的工具集、记忆状态、工作目录跟上一轮一模一样。

问题的本质是:system prompt 不是一段静态说明,而是当前运行环境的配置快照。它应该反映此刻 Agent 到底有哪些工具、工作目录在哪、有没有加载记忆、是否处在特殊模式。硬编码会带来三个直接后果——可维护性差(改工具描述可能误伤身份说明)、不够动态(没有.memory/MEMORY.md时还在告诉模型"这里有记忆")、浪费 token(每轮都把所有能力说明塞进去,无关内容还会分散模型注意力)。

s10 的解法是把 prompt 拆成多个可独立维护、按需加载、可缓存复用的 section,再用json.dumps做稳定序列化来判断"配置有没有变"。这篇文章我会把 settings.json 骨架、TaoToken 统一 Key 接入、缓存命中验证动作完整走一遍,你可以直接复制到自己的项目里跑。

2. TaoToken 前置:统一 Key 与 API 通道

在动手改 prompt 之前,先把模型调用通道固定下来。learn-claude-code 的示例默认走 Anthropic 的 messages 接口,如果你本地同时试多个模型、多个项目,Key 散落在各处会很乱。我的做法是统一走 TaoToken 的 API 通道,一个 Key 覆盖对话和编码场景。

你需要先拿到 Key:打开 https://taotoken.net/api-keys ,登录后创建一个 API Key,复制保存。注意这个 Key 只在创建时完整显示一次,丢了就重新建一个。

拿到 Key 之后,把它写进环境变量,不要硬编码进代码:

export TAOTOKEN_API_KEY="sk-你的key" export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_API_KEY="$TAOTOKEN_API_KEY"

这里有个细节:learn-claude-code 的示例代码用的是anthropicSDK,它默认读ANTHROPIC_API_KEY和ANTHROPIC_BASE_URL。把 base_url 指向 TaoToken 的 API 地址,SDK 就会把请求发到统一通道,你不用改任何业务代码。如果你用的是 Claude Code 这类命令行工具,接入方式在 https://taotoken.net/doc 里有对应说明,配置逻辑是一样的。

注意:base_url 只写到/api,不要自己拼/v1/messages之类的路径,SDK 会处理。

3. 可复制的 settings.json 骨架与 prompt 组装代码

先给一份 settings.json 骨架,把模型、工具、prompt section 都外置成配置,这样改工具描述不用动 Python 代码:

{ "model": "claude-sonnet-4-20250514", "max_tokens": 8000, "prompt_sections": { "identity": "You are a coding agent. Act, don't explain.", "tools": "Available tools: bash, read_file, write_file.", "workspace": "Working directory: {workspace}" }, "memory": { "index_file": ".memory/MEMORY.md", "enabled": true } }

workspace用占位符,运行时替换成真实路径。接下来是核心的组装与缓存逻辑,我按 s10 的思路整理成可直接跑的版本:

import json from pathlib import Path _last_context_key = None _last_prompt = None def load_settings(path="settings.json"): with open(path, "r", encoding="utf-8") as f: return json.load(f) SETTINGS = load_settings() PROMPT_SECTIONS = SETTINGS["prompt_sections"] def update_context(context: dict, messages: list) -> dict: """每轮工具调用后重新读取记忆文件,刷新 context。""" memory_file = Path(SETTINGS["memory"]["index_file"]) if SETTINGS["memory"]["enabled"] and memory_file.exists(): content = memory_file.read_text(encoding="utf-8").strip() context["memories"] = content else: context["memories"] = "" return context def assemble_system_prompt(context: dict) -> str: sections = [] sections.append(PROMPT_SECTIONS["identity"]) sections.append(PROMPT_SECTIONS["tools"]) sections.append( PROMPT_SECTIONS["workspace"].format(workspace=context.get("workspace", ".")) ) memories = context.get("memories", "") if memories: sections.append(f"Relevant memories:\n{memories}") return "\n\n".join(sections) 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: print(" [cache hit] system prompt unchanged") return _last_prompt _last_context_key = key _last_prompt = assemble_system_prompt(context) loaded = ["identity", "tools", "workspace"] if context.get("memories"): loaded.append("memory") print(f" [assembled] sections: {', '.join(loaded)}") return _last_prompt

这里最关键的一行是json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)。三个参数各有用途:sort_keys=True保证{"b":2,"a":1}和{"a":1,"b":2}生成完全相同的字符串,顺序不影响比较;ensure_ascii=False保留中文,不转成\u7528\u6237,让指纹可读;default=str处理Path这类特殊对象,避免序列化报错。

为什么不用 Python 内置的hash()?因为hash()有进程随机化,同一个字典在不同进程里哈希值不同,而且嵌套 dict/list 会直接报错。json.dumps生成的是纯文本指纹,跨进程稳定,嵌套结构也能处理。

把组装逻辑接进 agent loop,注意每轮工具调用后要重新评估 context:

def agent_loop(messages: list, context: dict): system = get_system_prompt(context) while True: response = client.messages.create( model=SETTINGS["model"], system=system, messages=messages, tools=TOOLS, max_tokens=SETTINGS["max_tokens"], ) messages.append({"role": "assistant", "content": response.content}) if response.stop_reason != "tool_use": return results = [] for block in response.content: if block.type != "tool_use": continue handler = TOOL_HANDLERS.get(block.name) output = handler(**block.input) if handler else f"Unknown: {block.name}" results.append({ "type": "tool_result", "tool_use_id": block.id, "content": output, }) messages.append({"role": "user", "content": results}) context = update_context(context, messages) system = get_system_prompt(context)

4. 验证请求:缓存命中与未命中的实际输出

代码写完了,怎么确认缓存真的生效?最直接的办法是看控制台打印。第一次调用时 context 是全新的,会走组装分支:

[assembled] sections: identity, tools, workspace

紧接着再调一次,context 没变,应该命中缓存:

[cache hit] system prompt unchanged

我实测下来,连续两轮没有工具写入记忆时,第二轮一定是 cache hit。一旦某个工具往.memory/MEMORY.md写了新内容,update_context读到变化,指纹就变了,下一轮会重新组装,并且 sections 里多出 memory:

[assembled] sections: identity, tools, workspace, memory

如果你想更严谨地验证,可以在get_system_prompt里加一行打印指纹长度,或者把key写进日志文件对比。另一个验证角度是看 API 请求的 token 消耗:缓存命中时你复用的是同一个字符串对象,虽然发给模型的 token 数不变(system prompt 本身还是要传),但省掉了本地重复拼接的 CPU 开销,更重要的是保证了 section 顺序稳定,这对 API 层的 prompt cache 命中很关键。

提示:s10 的进程内缓存只避免重复字符串组装。真正的 API 级 prompt cache 还需要稳定的 section 顺序和动态边界标记,Claude Code 官方用SYSTEM_PROMPT_DYNAMIC_BOUNDARY来区分静态段和动态段,你可以把 identity、tools 这类不变内容放前面,memory、workspace 放后面。

5. 本篇常见错排查

报错一:TypeError: Object of type PosixPath is not JSON serializable

原因是你往 context 里塞了Path对象,而json.dumps默认不认识。解决方法是加default=str,它会调用str()把 Path 转成普通字符串。如果你塞的是自定义类,也可以传一个 lambda 做转换。

报错二:缓存永远不命中,每轮都打印 assembled

先检查 context 里有没有每次都变的字段,比如时间戳、随机 ID、请求计数。这些字段一变,指纹就变。把它们从 context 里剔除,或者单独放到不参与指纹计算的地方。另一个常见原因是字典键顺序不稳定,虽然sort_keys=True能解决,但如果你在别处手动拼了字符串再塞进 context,顺序就乱了。

报错三:记忆更新了但 prompt 没变

检查update_context是不是在每轮工具调用后都执行了。s10 的 loop 里,context = update_context(context, messages)和system = get_system_prompt(context)必须放在messages.append之后、下一轮client.messages.create之前。漏掉这一步,记忆文件改了也不会重新读取。

报错四:中文记忆内容变成\uXXXX

这是ensure_ascii默认True导致的。加上ensure_ascii=False就能保留中文。注意这个参数只影响序列化后的可读性,不影响比较结果,但保留中文能让日志更好排查。

报错五:多进程下缓存失效

_last_context_key和_last_prompt是模块级全局变量,只在单个进程内有效。如果你用多进程跑 Agent,每个进程有独立的缓存,这是正常的。跨进程共享缓存需要外部存储,但通常没必要,因为进程内缓存已经能覆盖大部分重复组装场景。

6. 把通道和缓存一起固定下来

到这里,你的 Agent 应该已经能做到:context 不变时复用 system prompt,context 变化时按需重新组装,并且所有模型请求都走统一的 Key 和 API 通道。这套组合的价值在于,你把"配置快照"和"调用通道"两件事都从业务代码里解耦出来了。

接下来可以做的验证动作:跑一轮带工具调用的对话,观察控制台先出现[assembled],工具执行后如果没写记忆,下一轮出现[cache hit];手动往.memory/MEMORY.md追加一行,再跑一轮,应该看到[assembled] sections: identity, tools, workspace, memory。如果这三步都符合预期,说明缓存逻辑和记忆刷新都接对了。

模型对话调试可以直接在 https://taotoken.net/model-chat 里对比不同 system prompt 的效果;长期跑编码类 Agent 的话,https://taotoken.net/coding-plan 里有按周期计费的方案,比按量付费更适合高频调用;接入文档和参数细节在 https://taotoken.net/doc 里能查到。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 21:42:01

中文对话 代替函数公式 Excel数据分析新范式

TOOL 15 AI办公 2026.09 函数公式背到吐中文对话 代替函数公式 Excel数据分析新范式ChatExcel 通义千问 飞书AI CopilotExcel数据分析聊天式 零门槛 核心观点你不是不会用Excel,你是不想花时间记公式 2026年AI Excel工具已分化为三条路径,选对效率…

作者头像 李华
网站建设 2026/9/29 21:37:03

一次跨国雇佣是怎么走完的:从合规卡点到海外落地的全流程复盘

很多出海企业在招到海外第一个核心员工时,都会经历一段过山车式的心情。前一秒还在为挖到了当地资深的销售负责人或售后工程师开香槟,下一秒就被财务和法务的一连串灵魂发问给问懵了:我们在目标国家没有注册公司实体,没有当地银行…

作者头像 李华