1. 多轮 Agent 为什么总在第三轮开始“失忆”
如果你用 LangGraph 搭过多轮 Agent,大概率遇到过这个场景:第一轮聊得好好的,第二轮还记得用户叫小明、在做 TypeScript 项目,到第三轮突然开始问“请问您怎么称呼”。这不是模型变笨了,而是上下文窗口被对话历史挤满,早期的关键信息被压缩掉了。
LangGraph 本身提供了 checkpointer 做状态持久化,但默认的 MemorySaver 只活在进程内存里,重启即丢;就算换成 SqliteSaver,它存的也是整条消息流,而不是“提炼后的事实”。结果就是:状态是持久的,但认知是碎片化的。用户说“以后简洁一点”,这条偏好混在几十条消息里,下一轮构造 system prompt 时根本捞不出来。
OpenClaw 的 Learning & Adaptation 机制解决的正是这个问题。它把 Agent 的“内部状态”拆成八类 Markdown 文件,每类有明确的职责、读写权限和更新策略,形成一个可被 Agent 自己读写的“人格操作系统”。核心思路是:不靠强化学习,也不做显式微调,而是通过文件优先的认知结构 + Agent 自修改 + 记忆蒸馏,在运行时持续积累经验、调整行为、扩展能力。
这篇文章面向正在用 LangGraph 编排多轮 Agent、并且希望 Agent 能“记住偏好、越用越顺手”的开发者。我会拆解 OpenClaw 的 Memory 持久化机制和 Self-Modification 触发条件,给出可复制的配置片段,并演示一轮对话后状态回写与行为调整的完整验证步骤。模型调用部分统一走 TaoToken 的 API 通道,一个 Key 覆盖多家模型,省去多平台切换的麻烦。
适合谁看:已经跑通 LangGraph 基础图、想给 Agent 加上长期记忆和自适应能力的同学;或者正在评估 OpenClaw 这类文件驱动 Agent 架构、想知道它和 LangGraph 原生方案差异的同学。下面从机制拆到代码,尽量做到跟着敲就能跑。
2. TaoToken 前置:统一 Key 与 API 通道接入
在动手改 Memory 配置之前,先把模型调用通道理顺。OpenClaw 和 LangGraph 都会频繁发起 LLM 请求,如果每个模型单独配 Key、单独改 Base URL,调试成本会很高。TaoToken 提供统一的 API 入口,兼容 OpenAI 风格的请求格式,一个 Key 就能调用多种模型。
先拿到 Key。访问 https://taotoken.net/api-keys 创建 API Key,复制保存。注意这个 Key 只在创建时完整显示一次,丢了只能重建。
Base URL 统一填https://taotoken.net/api,不要带任何路径后缀。模型 ID 按你实际要用的填,比如claude-sonnet-4-5、gpt-4o、deepseek-chat等,具体以控制台模型列表为准。三件套记牢:Base URL + Key + Model ID,后面所有配置都围绕这三个值展开。
如果你用 Claude Code 做辅助开发,可以在 settings 里配置环境变量指向 TaoToken;如果用 Cline 或带 MCP 的编辑器,同样在 MCP server 配置里填这三个值。Codex 用户则在~/.codex/auth.json里配置。不管哪种客户端,本质都是把请求转发到https://taotoken.net/api,由 TaoToken 路由到对应模型。
在 LangGraph 侧,最省事的做法是用ChatOpenAI并覆盖base_url:
from langchain_openai import ChatOpenAI llm = ChatOpenAI( model="claude-sonnet-4-5", api_key="sk-你的TaoTokenKey", base_url="https://taotoken.net/api", temperature=0.3, )在 OpenClaw 侧,模型配置写在openclaw.json的models段里,同样填这三个值。这样 LangGraph 图和 OpenClaw Agent 共用同一个通道,日志和用量集中在一处,排查问题时不用在多个平台之间跳。
有一点要提醒:TaoToken 是模型调用通道,不是编辑器替代品,也不做代码补全。它的定位是让你在 Agent 编排里稳定地拿到模型响应。配置完成后,先用一个最小请求验证通道是否通,再往下做 Memory 配置。
3. 可复制配置:Memory 持久化与 Self-Modification 开关
这一节给出可以直接抄的配置片段。OpenClaw 的配置集中在openclaw.json,LangGraph 侧则用 Python 代码控制。两者配合的方式是:OpenClaw 负责文件级记忆读写,LangGraph 负责编排节点和触发时机。
先看 OpenClaw 的 Memory Flush 配置。这是 Learning 机制里最关键的一环:当会话 token 接近 compaction 阈值时,系统会发起一次不展示给用户的 Agent 轮次,提示模型“把重要内容写入记忆”。
{ "agents": { "defaults": { "compaction": { "memoryFlush": { "enabled": true, "softThresholdTokens": 4000, "systemPrompt": "Session nearing compaction. Store durable memories now.", "prompt": "Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store." } } } } }触发条件是:当 session 的 token 估算超过contextWindow - reserveTokensFloor - softThresholdTokens时,触发一次 flush。执行流程是系统检测到上下文即将被压缩,发起一个不向用户展示的 Agent 轮次,提示模型把持久性笔记写入memory/YYYY-MM-DD.md或MEMORY.md,模型通常用NO_REPLY结束,用户看不到这次轮次,随后执行 compaction,旧消息被摘要,新记忆已落盘。
再看 Self-Modification 的开关。OpenClaw 的 Agent 具备 read、write、edit、bash 等工具,技术上可以修改工作区内的任何文件,包括SOUL.md。如果你希望限制 Agent 对核心人格文件的写权限,可以在配置里做约束:
{ "agents": { "defaults": { "selfModification": { "enabled": true, "writableFiles": ["AGENTS.md", "MEMORY.md", "USER.md"], "protectedFiles": ["SOUL.md", "IDENTITY.md"], "requireConfirmation": false } } } }writableFiles列出允许 Agent 自主修改的文件,protectedFiles列出禁止写入的文件。如果你希望人格稳定,把SOUL.md放进 protected;如果你希望 Agent 能根据交互进化,就把它放进 writable。这个取舍后面第 7 节会展开。
LangGraph 侧的 Memory 节点配置。我们用SqliteSaver做状态持久化,并在图里加一个update_preference节点,把用户偏好写进外部 JSON:
import json from langgraph.graph import StateGraph, END from langgraph.checkpoint.sqlite import SqliteSaver PREF_PATH = "./adaptive_preferences.json" def load_prefs(): try: with open(PREF_PATH, "r", encoding="utf-8") as f: return json.load(f) except FileNotFoundError: return {"detail_level": 5, "tone": 5, "technical_depth": 5, "structure": 5} def save_prefs(prefs): with open(PREF_PATH, "w", encoding="utf-8") as f: json.dump(prefs, f, ensure_ascii=False, indent=2) def update_preference(state): prefs = load_prefs() delta = state.get("pref_delta", {}) for k, v in delta.items(): if k in prefs: prefs[k] = max(0, min(10, prefs[k] + v)) save_prefs(prefs) return {"preferences": prefs}把update_preference接到图里,用户说“太啰嗦了,简单点”,LLM 解析出{"detail_level": -1},节点执行后detail_level从 5 降到 4,下次构造 system prompt 时按新值调整。这就是 LangGraph 版的 Learning 闭环。
OpenClaw 和 LangGraph 的分工可以这样理解:OpenClaw 管文件级记忆(八类 Markdown),LangGraph 管图级编排(节点、边、状态流转)。两者通过共享的MEMORY.md和adaptive_preferences.json交换信息。配置完成后,先跑一轮对话验证状态回写。
4. 验证请求:一轮对话后的状态回写与行为调整
配置写完不算完,得验证它真的生效。这一节演示从发请求到状态回写的完整链路,每一步都有可观察的结果。
第一步,启动 LangGraph 图并带上 checkpointer。用thread_id区分会话:
from langgraph.checkpoint.sqlite import SqliteSaver with SqliteSaver.from_conn_string("checkpoints.sqlite") as saver: graph = build_graph().compile(checkpointer=saver) config = {"configurable": {"thread_id": "user-xiaoming-001"}} result = graph.invoke( {"messages": [("user", "以后回答简洁一点,别啰嗦")]}, config=config, ) print(result["preferences"])预期输出里detail_level应该从 5 变成 4。如果没变,检查update_preference节点是否真的被触发,以及pref_delta是否被 LLM 正确解析。
第二步,验证 OpenClaw 的 Memory Flush。手动把会话 token 推到阈值附近,观察memory/目录下是否生成当天的 Markdown 文件:
ls -la memory/ cat memory/2025-01-15.md如果 flush 生效,文件里应该有模型写入的持久性笔记,比如“用户偏好简洁回复”“用户在做 LangGraph 项目”。注意 flush 轮次对用户不可见,所以你在对话里看不到“正在保存记忆”之类的提示,只能通过文件变化确认。
第三步,验证行为调整。发第二轮请求,观察回复风格是否变化:
result2 = graph.invoke( {"messages": [("user", "帮我解释一下 LangGraph 的 checkpointer")]}, config=config, ) print(result2["messages"][-1].content)如果detail_level已降到 4,回复应该比默认更短、更直接。你可以对比调整前后的输出长度和结构,确认偏好真的影响了 system prompt 的构造。
第四步,验证 Self-Modification。让 Agent 修改AGENTS.md,加入一条“Things I've Learned”:
# 对话中触发 # 用户:记住,staging 服务器在 192.168.1.42 # 然后检查文件 cat AGENTS.md | grep -A3 "Things I've Learned"如果 Agent 有写权限,AGENTS.md里应该多出一条记录。注意这个修改在下一次 Agent 运行才生效,当前 run 用的还是旧版本。这是 OpenClaw 的设计:Agent 为“未来的自己”编程。
验证通过后,你会看到一条完整链路:用户表达偏好 → LLM 解析为维度调整 → 写入持久化文件 → 下次构造 prompt 时读取 → 行为改变。这就是 Learning & Adaptation 的最小闭环。
5. 本篇常见错排查:401、local proxy failed 与 reading choices
配置和验证过程中,最容易卡在几个报错上。这一节按真实报错逐个排查,每个都给定位方法和修复步骤。
401 Unauthorized。最常见的原因是 Key 没填对或 Base URL 写错。先确认api_key是完整的sk-开头字符串,没有多余空格;再确认base_url是https://taotoken.net/api,不要带/v1或其他后缀。如果用的是环境变量,检查变量名是否和代码里读的一致。还有一种情况是 Key 被禁用或额度耗尽,去控制台确认状态。
local proxy failed。这个报错通常出现在客户端配置了本地代理但代理没启动,或者代理地址填错。检查你的客户端设置里是否残留了http://127.0.0.1:xxxx之类的代理配置,如果有就清掉,让请求直连https://taotoken.net/api。另外确认网络环境能正常访问该域名,可以用curl -I https://taotoken.net/api测试连通性。
reading choices 报错。典型信息是KeyError: 'choices'或list index out of range,说明返回的 JSON 里没有choices字段。原因通常是请求被路由到了非预期端点,或者模型 ID 填错导致返回了错误结构。检查model字段是否和控制台模型列表一致,Base URL 是否被意外改写。如果用的是 LangChain 的ChatOpenAI,确认没有额外传openai_api_base造成冲突。
OAuth 相关报错。如果你在 Claude Code 或 Codex 里看到 OAuth 失败,通常是因为客户端尝试走官方登录流程,而不是用 API Key。解决办法是在配置里显式指定 API Key 模式,把 Base URL 指向https://taotoken.net/api,并填入 TaoToken 的 Key。Codex 用户检查~/.codex/auth.json里的字段是否完整,Claude Code 用户检查 settings 里的环境变量。
Memory Flush 不触发。如果memory/目录一直空着,先确认memoryFlush.enabled是true,再确认 token 真的超过了阈值。可以临时把softThresholdTokens调小(比如 500)来测试。另外检查memory/目录是否有写权限,容器环境下路径映射是否正确。
Self-Modification 不生效。Agent 改了文件但行为没变,先确认修改的是不是protectedFiles里的文件。如果是SOUL.md被保护,Agent 的写入会被拒绝。再确认修改在下一次 run才生效,当前 run 不会重新读取人格文件。最后检查skills.load.watch是否为true,这影响 skill 的热加载。
排查时建议开日志。LangGraph 侧用langchain.debug = True,OpenClaw 侧看 Gateway 日志。日志里能看到每次请求的 Base URL、模型 ID 和返回状态,定位问题比猜快得多。
6. 语义一致 CTA:把通道和文档用起来
配置跑通之后,日常开发里最常打交道的两个入口是 API Keys 和接入文档。Key 管理在 https://taotoken.net/api-keys,接入细节在 https://taotoken.net/doc。建议把这两个页面存书签,改配置时直接对照,比翻聊天记录快。
如果你主要在做模型验证和对比,比如想确认某个模型在 Memory 摘要任务上的表现,可以用模型对话页面快速试:https://taotoken.net/models。同一个 prompt 换不同模型跑一遍,看哪个的摘要更准、更省 token,再决定写进配置。
如果你长期做编码类 Agent,或者要跑带工具调用的复杂图,Coding Plan 更合适:https://taotoken.net/coding-plan。它针对长会话和高频调用做了优化,配合 OpenClaw 的 Memory Flush 和 LangGraph 的 checkpointer,能撑住多轮、长周期的 Agent 场景。
Claude Code 用户如果要做接入,参考 https://taotoken.net/claude-code;需要看 Anthropic 兼容细节的,参考 https://taotoken.net/anthropic。控制台在 https://taotoken.net/console,用量和日志都在那里。
最后说一个实操经验:Memory 文件不要写太长。MEMORY.md建议控制在几千字以内,超出的内容拆到memory/YYYY-MM-DD.md按日归档。OpenClaw 的 100-Tick Distillation 机制就是干这个的——每积累一定轮次就把记忆压缩一次,只保留最关键的策略和事实。你自己实现时也可以照这个思路,定期让 LLM 对MEMORY.md做一次摘要,把冗余信息清掉。记忆不是越多越好,能捞出来用上的才是好记忆。