ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

LangChain 记忆越拼越费 token?用 TaoToken 接的 Codex 照着 ConversationBufferWindowMemory 的 k 值改

2026/9/18 14:02:46 拓冰建站 浏览量
LangChain 记忆越拼越费 token?用 TaoToken 接的 Codex 照着 ConversationBufferWindowMemory 的 k 值改 LangChain 项目里用 ConversationBufferMemory 存对话前几轮没什么感觉聊到二三十轮之后每轮提示词里的 token 数开始直线上升账单跟着涨最后接口直接返回上下文窗口超限。要复现并改掉这个坑可以先用 TaoToken 把 Codex 和 LangChain 里的 chat model 都接到统一入口 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentlangchain-memory-k 再照着 ConversationBufferWindowMemory 的 k 值逐段调整。记忆管理这件事不只是“存不存历史”而是“每轮到底把多少历史重新塞回提示词”。下面按排障顺序走一遍先看清 token 为什么越聊越贵再让 Codex 读你的 memory 代码做对照最后把窗口记忆、总结记忆、session_id 过期清理和 Redis 落地方案串起来。1. ConversationBufferMemory 为什么让 token 越聊越多1.1 从一次上下文超限的报错说起现象通常很一致本地调试没问题一上多轮对话前几轮响应正常第十轮之后响应开始变慢第二十轮左右接口返回类似maximum context length exceeded或context_length_exceeded的报错。此时如果去打印每轮请求的 messages会发现 system 消息没变但历史消息数组一轮比一轮长用户和 AI 的每句话都被原样保留。ConversationBufferMemory 的默认行为就是把每轮对话全量追加进chat_history下次调用时再把整个chat_history拼回提示词。它的优点是没有信息丢失缺点是 token 随轮次线性增长。假设单轮对话平均 120 token聊到 50 轮光是历史就可能吃掉 6000 token再叠加 system prompt、工具说明、检索内容很容易顶到模型窗口上限。具体窗口大小和计费规则以模型广场当时列表为准不要凭记忆写死一个上限。1.2 全量 buffer 的拼接路径在 LangChain 里链路通常是这样用户输入 → memory.load_memory_variables → 得到chat_history→ 和当前问题一起送进 prompt → 调 chat model → 输出再写回 memory。问题就出在load_memory_variables返回的是全量历史而不是最近几轮。很多项目在早期为了快速跑通会写成全局单例 memoryfrom langchain.memory import ConversationBufferMemory memory ConversationBufferMemory( memory_keychat_history, return_messagesTrue, )单用户本地跑没问题一旦线上多个用户共用这个memory对象还会出现串会话A 用户问完B 用户一进来A 的历史也跟着进了 B 的提示词。到了这一步问题不只是贵而是正确性和成本同时失控。2. 把 Codex 接到 TaoToken让它读 memory 代码2.1 创建 Key 与 Codex 的 config.toml先打开 TaoToken 注册并创建 API Key得到的 Key 先记成占位符YOUR_API_KEY不要直接写进提交到 Git 的代码里。Codex 这边改~/.codex/config.toml把供应商指向兼容通道Base URL 填https://taotoken.net/api末尾不要带/v1也不要在这条地址上加任何 UTM 参数。model YOUR_MODEL_ID model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY然后在终端里设置环境变量Key 用你刚创建的那把export TAOTOKEN_API_KEYYOUR_API_KEYYOUR_MODEL_ID不要随便编一个带日期的名字直接去 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentlangchain-memory-k 的模型广场看当前可用列表把对应 ID 复制过来。这样 Codex 才能通过 TaoToken 读到你的项目文件而不是在本地靠猜。2.2 让 Codex 先做判断窗口记忆还是总结记忆Codex 在这里的定位是“读代码、解释改动、生成补丁建议”不是直接连你的生产服务去执行操作。可以这样给它指令请逐段阅读memory_chain.py里 memory 的创建、读取、写回位置标出哪些地方把全量历史拼进了 prompt对照 ConversationBufferWindowMemory 的 k 值方案判断当前场景适合“只保留最近几轮”还是“窗口记忆 总结记忆”组合。判断标准可以交给它一起梳理如果业务只需要最近上下文比如客服短会话、命令式问答优先窗口记忆如果业务必须记住早期用户偏好、订单号、长期设定就加一层总结记忆把旧历史压缩成摘要而不是继续全量保留。Codex 生成改动建议后你在本地看 diff确认没有把memory_key、session_id、Redis key 前缀改乱再落盘。3. ConversationBufferWindowMemory 的 k 值怎么定3.1 k 值不是越大越好ConversationBufferWindowMemory 的核心参数就是k只保留最近 k 轮对话。这里的“轮”通常按消息条数或对话轮次计数具体看你的 LangChain 版本和调用方式。k 越大上下文越完整token 也越贵k 越小成本越低但可能丢掉刚发生的重要信息。一个常见的错误是直接把 k 设成 50、100觉得“反正只是最近几轮”。如果每轮消息很长k20 就可能把窗口吃满。先把 k 设小再按业务回归而不是反过来。对于大多数问答型应用可以从 k6 或 k8 起步观察两件事回答是否还引用得到上一轮的关键约束每轮 prompt token 是否稳定在一个区间而不是继续线性增长。3.2 从 6 开始按轮次和 token 回归把原来的 memory 替换成窗口记忆示例from langchain.memory import ConversationBufferWindowMemory memory ConversationBufferWindowMemory( k6, memory_keychat_history, return_messagesTrue, )替换后不要只看接口是否返回 200要构造一组多轮请求同一 session_id 连续问 20 轮每轮都打印 memory 里的消息条数和 prompt 的大致长度。如果第 10 轮和第 20 轮的 prompt 长度基本持平说明窗口已经生效如果还在涨先检查是不是 prompt 模板里又手动拼了一次完整历史或者memory_key和模板变量名不一致。k 值最终取多少没有统一答案。短指令场景 k4 可能就够长文档问答 k8 到 10 更稳。关键是把它当成一个可回归的参数而不是一次拍脑袋写死。改完 k 值后用同一批测试问题跑前后对比回控制台看这次调用的 token 用量确认成本曲线被压平。4. 总结记忆与 session_id、last_active 的过期清理4.1 ConversationSummaryBufferMemory 压缩旧历史窗口记忆解决“只留最近几轮”但有些业务不能丢早期信息。这时用 ConversationSummaryBufferMemory它保留最近一段原始对话同时把更早的历史交给 LLM 压缩成摘要。注意这个 summarization 调用本身也会消耗 token所以用来做总结的 llm 也应该走同一个兼容通道Base URL 同样填https://taotoken.net/apiKey 用YOUR_API_KEY。from langchain.memory import ConversationSummaryBufferMemory from langchain_openai import ChatOpenAI llm ChatOpenAI( modelYOUR_MODEL_ID, base_urlhttps://taotoken.net/api, api_keyYOUR_API_KEY, temperature0.2, ) memory ConversationSummaryBufferMemory( llmllm, max_token_limit1200, memory_keychat_history, return_messagesTrue, )max_token_limit控制什么时候触发压缩。设得太高摘要迟迟不触发等于还在全量存设得太低摘要调用频繁成本反而上去。可以先用窗口记忆跑出 token 基线再把总结记忆的阈值定在基线的 1.5 到 2 倍附近然后根据实际账单微调。4.2 本地字典版 session 过期多用户场景不能用一个全局 memory。至少要用session_id做隔离并用last_active做过期清理否则用户走了历史还在内存里占着。下面是一个本地字典版的简化写法适合单机调试import time from langchain.memory import ConversationBufferWindowMemory SESSION_TTL 1800 _sessions {} def get_memory(session_id: str): now time.time() entry _sessions.get(session_id) if entry and now - entry[last_active] SESSION_TTL: _sessions.pop(session_id, None) entry None if entry is None: entry { memory: ConversationBufferWindowMemory( k6, memory_keychat_history, return_messagesTrue, ), last_active: now, } _sessions[session_id] entry entry[last_active] now return entry[memory]每次请求前用session_id取 memory请求结束更新last_active。注意不要把这个字典当成生产方案它只适合单进程进程一重启记忆就没了。4.3 Redis 版多实例记忆线上多实例部署时本地字典会失效因为下一次请求可能落到另一台机器。这时把会话消息落到 Rediskey 带上 session_id 和业务前缀并设置 TTL。Codex 可以帮你生成读写 Redis 的辅助函数但连接 Redis、执行清理命令仍然由你在本地或跳板机上完成不要让 AI 工具直接连生产库执行删除。import json import redis from langchain_core.messages import HumanMessage, AIMessage r redis.Redis(hostlocalhost, port6379, decode_responsesTrue) TTL_SECONDS 1800 def append_message(session_id: str, role: str, content: str): key fchat:memory:{session_id} payload json.dumps({role: role, content: content}, ensure_asciiFalse) r.rpush(key, payload) r.expire(key, TTL_SECONDS) def load_recent(session_id: str, limit: int 12): key fchat:memory:{session_id} items r.lrange(key, -limit, -1) messages [] for item in items: data json.loads(item) if data[role] user: messages.append(HumanMessage(contentdata[content])) else: messages.append(AIMessage(contentdata[content])) return messages这里只保留最近limit条消息和窗口记忆的 k 值思路一致。Redis 的 TTL 负责过期清理last_active可以单独存一个时间戳 key也可以直接依赖 TTL。上线前先用测试 session 跑通确认真实请求只读取当前用户的 key不会串到别人的历史。5. 把 LangChain chat model 的 base_url 指向 https://taotoken.net/api5.1 ChatOpenAI 的 base_url 与 api_keyLangChain 应用里真正烧 token 的是 chat model所以它的 base_url 也要一起改。以 ChatOpenAI 兼容方式为例from langchain_openai import ChatOpenAI llm ChatOpenAI( modelYOUR_MODEL_ID, base_urlhttps://taotoken.net/api, api_keyYOUR_API_KEY, temperature0.2, max_tokens1024, )YOUR_MODEL_ID仍然以模型广场当时列表为准不要自己拼一个不存在的名称。api_key用从 TaoToken 创建的那把不要和 Codex 的配置混成两套不同来源。这样 LangChain 链路、Codex 辅助阅读、总结记忆里的 LLM 调用都走同一个入口排查 token 用量时不会因为多个供应商而看花眼。5.2 不要把 /v1 带进 Base URL一个高频错误是把 Base URL 写成https://taotoken.net/api/v1。这里填进工具的地址是https://taotoken.net/api末尾不要加/v1也不要加 UTM 参数。UTM 参数只用于给人点的官网落地页比如注册、创建 Key、看模型广场接口地址不需要也不应该带这些查询参数。如果调用后返回 404 或模型不存在先检查 base_url 是否被多拼了路径如果返回 401检查YOUR_API_KEY是否复制完整以及环境变量是否真的在当前进程里生效。改完配置后不要只测一次单轮问答要跑多轮会话确认 memory 的读写和模型调用都正常。6. 跑多会话请求回控制台看用量6.1 观察每轮 prompt 的长度曲线验证记忆改造是否生效最直接的方法是构造多会话请求。准备三个 session_id每个 session 连续问 15 到 20 轮每轮记录当前 memory 里的消息条数、prompt 的 token 估算、接口返回的 token 用量。改造前消息条数会随轮次一直涨换成 ConversationBufferWindowMemory 后消息条数应该在 k 值附近稳定下来。如果你在 LangChain 里用了回调可以打印总 tokenfrom langchain_community.callbacks import get_openai_callback with get_openai_callback() as cb: result chain.invoke({input: 继续上一轮的问题}) print(cb.total_tokens)如果回调拿不到数据就退一步数 memory 里的消息条数并观察接口响应里的 usage 字段。重点不是某一次调用的绝对数字而是第 5 轮、第 10 轮、第 20 轮的曲线是否变平。变平说明窗口或总结在起作用继续线性上升说明还有全量历史被拼进提示词。6.2 排障记忆还在涨、串会话、调用失败第一种情况换成窗口记忆后 token 还在涨。常见原因是 prompt 模板里自己拼了chat_history同时 memory 又注入了一次或者memory_key和模板变量名不一致导致 memory 没被用上但业务代码手动把历史塞进了字符串。让 Codex 对照 memory 创建处和 chain 调用处把两处变量名对齐。第二种情况多用户串会话。检查是不是还在用全局单例 memory或者 Redis key 没带 session_id。所有记忆读写都必须以当前请求的 session_id 为维度过期清理也要按这个维度做。第三种情况调用失败。401 先看 Key 是不是从官网创建的那把404 先看 Base URL 是否误写成带/v1的地址如果返回上下文超限先看 k 值是否过大或者总结记忆的max_token_limit设得太高。每次改完只回归一个变量避免同时改 k 值和 prompt 模板最后不知道是哪一步生效。7. 下一步用同一把 Key 做模型对话和 Coding Plan7.1 模型对话先验证模型 ID记忆改造跑通后先用同一把 Key 去 模型对话 发一条测试消息确认模型 ID、Base URL、Key 三件套没有填错。这一步和 LangChain 里的调用是同一套参数能快速排除配置层问题。如果模型对话正常但 LangChain 仍然报错问题就在 memory 拼装或 prompt 模板而不是通道本身。7.2 Key 与套餐入口长期跑多会话应用Key 建议在 控制台 API Keys 里统一创建和管理不要和临时测试 Key 混用。如果这个 LangChain 服务还要承接写代码、跑 Codex 辅助阅读可以打开 Coding Plan 看套餐是否够用Codex 或 Claude Code 的接入参数对照可以再看 接入文档。最后提醒一句记忆改造不是把 k 值调小就结束。窗口记忆解决“最近几轮”总结记忆解决“更早历史压缩”session_id 与 TTL 解决“多用户隔离和过期”Redis 解决“多实例共享”。每一层都要在本地的多轮请求里回归过再回控制台对一次用量确认你看到的是记忆拼接长度在变而不是调用本身失败。