☰
用 LangGraph 破局!上下文工程四大高效调度策略,让 Agent 告别“记忆过载”
2026/10/7 14:13:37 网站建设 项目流程

1. 长会话 Agent 为什么会“记忆过载”:从一次工具调用雪崩说起

如果你正在用 LangGraph 搭多节点 Agent,大概率遇到过这种场景:前 5 轮对话一切正常,第 6 轮开始模型突然“失忆”,把已经确认过的参数又反问一遍;再往后直接报context_length_exceeded,或者更隐蔽地——不报错,但回答质量断崖式下跌,工具调用参数开始瞎编。

这不是模型变笨了,而是上下文窗口被塞爆了。LangGraph 的 State 默认是“只增不减”的:每个 Node 返回的messages都会 append 到全局状态里,工具返回的 JSON、检索到的文档片段、中间推理步骤,全都堆在同一个messages列表里。一个调用搜索工具的节点,单次返回 3000 token 的原始结果很常见,跑 10 轮就是 3 万 token,还没算系统提示词和工具 schema。

我把它拆成四个具体的“过载触发点”,你可以对照自己的图结构排查:

触发点一:工具结果无节制写入。搜索、数据库查询、网页抓取类工具,返回的是原始数据,不是结论。Agent 真正需要的可能只是其中 3 个字段,但整个 JSON 被塞进了 State。

触发点二:历史消息全量保留。多轮对话里,早期的寒暄、已废弃的方案、被否决的选项,对当前决策毫无价值,却持续占用窗口。

触发点三:多节点共享同一份上下文。规划节点、执行节点、校验节点如果读同一个messages,每个节点都会把自己的中间产物写回去,互相污染。

触发点四:召回策略缺失。需要历史信息时,要么全量塞入,要么完全不塞,没有“按相关性动态选取”的中间态。

这四类问题对应四种调度策略:滑动窗口、摘要压缩、向量召回、优先级队列。下面我用 LangGraph 的 State/Node 配置把它们逐个落地,并且把模型 endpoint 统一改到 TaoToken 的 Key 通道,方便你做对照实验——同一份图结构,换不同模型跑,观察上下文策略对成功率的影响。

先说清楚 TaoToken 在这里的角色:它是一个统一的模型 API 接入层,你拿一个 Key 就能调用多家模型,Base URL 是https://taotoken.net/api。对做上下文工程实验特别有用,因为你可以固定图结构和调度策略,只切换 Model ID,快速对比“同一策略在不同模型上的 token 消耗和任务完成率”。注册和拿 Key 在https://taotoken.net/api-keys,模型列表和对话测试在https://taotoken.net/models。

2. TaoToken 前置:统一 Key 通道与 LangGraph 环境准备

在写调度策略之前,先把模型通道打通。这一步不做,后面所有压测都没法横向对比。

2.1 拿 Key 与确认 Base URL

登录 TaoToken 控制台后,在 API Keys 页面创建一个 Key,形如sk-xxxxxxxx。记住两个地址:

  • Base URL(OpenAI 兼容):https://taotoken.net/api
  • 模型对话测试页:https://taotoken.net/models

LangGraph 本身不绑定模型,它通过ChatOpenAI这类封装调用。因为 TaoToken 兼容 OpenAI 协议,你只需要把base_url和api_key指过去,model填 TaoToken 支持的 Model ID 即可。

2.2 安装依赖

pip install langgraph langchain-openai langchain-core tiktoken

tiktoken用来做 token 计数,压测时统计上下文增长曲线,别省这一步。

2.3 环境变量配置

不要硬编码 Key。建一个.env:

TAOTOKEN_API_KEY=sk-你的key TAOTOKEN_BASE_URL=https://taotoken.net/api TAOTOKEN_MODEL=你的ModelID

然后在代码里读取。这样你切换模型只改一行环境变量,图结构完全不动,对照实验才干净。

2.4 验证通道是否通

写一个最小脚本,确认 Key 和 Base URL 正确:

import os from langchain_openai import ChatOpenAI from dotenv import load_dotenv load_dotenv() llm = ChatOpenAI( model=os.getenv("TAOTOKEN_MODEL"), api_key=os.getenv("TAOTOKEN_API_KEY"), base_url=os.getenv("TAOTOKEN_BASE_URL"), temperature=0, ) resp = llm.invoke("只回复两个字:通了") print(resp.content)

跑通再往下。如果这里报 401,先解决鉴权,别带着问题调调度策略,否则你分不清是策略问题还是通道问题。

2.5 为什么用统一通道做对照实验

上下文工程的核心指标是“在固定 token 预算下,任务完成率能到多少”。如果你每个模型用不同的 Key、不同的地址,变量太多。统一到 TaoToken 后,你的实验矩阵变成:

变量固定项变化项
图结构四个策略节点不变
调度策略滑动窗口/摘要/召回/队列逐个切换
模型Base URL + Key只换 Model ID
指标token 消耗、成功率、延迟记录对比

这样你才能说清楚“是策略起作用了,还是换模型碰巧好了”。

3. 可复制配置:四大调度策略的 State 与 Node 片段

这一节是核心。我把四种策略写成独立的 Node,你可以按需组合进同一张图。所有片段都基于 LangGraph 的StateGraph,State 用TypedDict+Annotated定义 reducer。

3.1 基础 State 定义

from typing import Annotated, TypedDict from langgraph.graph.message import add_messages from langchain_core.messages import BaseMessage class AgentState(TypedDict): messages: Annotated[list[BaseMessage], add_messages] summary: str retrieved: list[str] priority_buffer: list[dict] token_count: int

add_messages是 LangGraph 内置 reducer,负责把新消息合并进列表。注意:默认是追加,这正是“记忆过载”的根源。下面四个策略,本质都是在追加之前做拦截。

3.2 策略一:滑动窗口(Sliding Window)

思路最简单:只保留最近 N 轮消息,超出部分丢弃。适合工具调用密集、但历史依赖弱的场景。

from langchain_core.messages import SystemMessage WINDOW_SIZE = 12 # 保留最近12条消息 def sliding_window_node(state: AgentState) -> dict: msgs = state["messages"] if len(msgs) <= WINDOW_SIZE: return {"messages": msgs} # 永远保留 system message,其余按窗口截断 system_msgs = [m for m in msgs if isinstance(m, SystemMessage)] recent = msgs[-WINDOW_SIZE:] return {"messages": system_msgs + recent}

把它作为一个前置节点,插在 LLM 调用之前。注意 reducer 是add_messages,这里返回的messages会触发合并逻辑,所以更稳妥的写法是直接操作状态、用RemoveMessage清理旧消息。LangGraph 提供了RemoveMessage:

from langgraph.graph.message import RemoveMessage def sliding_window_node(state: AgentState) -> dict: msgs = state["messages"] if len(msgs) <= WINDOW_SIZE: return {} to_remove = msgs[:-WINDOW_SIZE] return {"messages": [RemoveMessage(id=m.id) for m in to_remove]}

这样才是真正“删除”,而不是靠覆盖。踩过的坑:早期我用返回新列表的方式,结果 reducer 把新旧列表合并了,窗口根本没生效,token 照样涨。

3.3 策略二:摘要压缩(Summarization)

滑动窗口会硬丢信息,如果被丢的里面有任务关键约束,Agent 就会跑偏。摘要压缩的做法是:把旧消息交给模型压缩成一段摘要,替换掉原始消息。

from langchain_core.messages import HumanMessage, AIMessage SUMMARIZE_THRESHOLD = 20 # 超过20条触发压缩 def summarize_node(state: AgentState) -> dict: msgs = state["messages"] if len(msgs) < SUMMARIZE_THRESHOLD: return {} old = msgs[:-6] # 保留最近6条原文 recent = msgs[-6:] prompt = ( "把以下对话压缩成不超过200字的摘要," "必须保留:已确认的参数、未完成的待办、被否决的方案。\n\n" + "\n".join(f"{m.type}: {m.content}" for m in old) ) summary = llm.invoke(prompt).content remove_ops = [RemoveMessage(id=m.id) for m in old] summary_msg = SystemMessage(content=f"[历史摘要] {summary}") return {"messages": remove_ops + [summary_msg], "summary": summary}

关键点:摘要提示词里明确要求保留“已确认参数、待办、被否决方案”,这三类是压缩时最容易丢、丢了又最致命的信息。你可以根据业务再加“用户偏好”“已调用过的工具名”。

3.4 策略三:向量召回(Vector Recall)

摘要压缩是“无损变有损”,向量召回是“按需取用”。把历史消息写入向量库,当前轮需要时,用语义相似度召回 top-k 片段注入上下文。

from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import FAISS embeddings = OpenAIEmbeddings( model="你的embedding模型ID", api_key=os.getenv("TAOTOKEN_API_KEY"), base_url=os.getenv("TAOTOKEN_BASE_URL"), ) def recall_node(state: AgentState) -> dict: query = state["messages"][-1].content # 假设 vectorstore 已在外层构建并持久化 docs = vectorstore.similarity_search(query, k=3) retrieved = [d.page_content for d in docs] context_msg = SystemMessage( content="[相关历史片段]\n" + "\n---\n".join(retrieved) ) return {"messages": [context_msg], "retrieved": retrieved}

注意:召回节点注入的是SystemMessage,不是HumanMessage,避免模型把它当成新一轮用户输入。另外 embedding 也走 TaoToken 通道,保持 Key 统一。

3.5 策略四:优先级队列(Priority Queue)

工具调用场景里,不同信息的“保鲜期”不同。工具返回的原始数据可能只在当前轮有用,而用户设定的目标要贯穿全程。优先级队列给每条消息打标签,按优先级决定保留顺序。

PRIORITY = {"goal": 3, "constraint": 3, "tool_result": 1, "chitchat": 0} def priority_node(state: AgentState) -> dict: buffer = state.get("priority_buffer", []) msgs = state["messages"] for m in msgs: level = classify(m) # 自定义分类函数 buffer.append({"id": m.id, "level": PRIORITY.get(level, 1)}) # 按优先级排序,低优先级且超出预算的删除 buffer.sort(key=lambda x: x["level"], reverse=True) keep_ids = {item["id"] for item in buffer[:BUDGET]} to_remove = [RemoveMessage(id=m.id) for m in msgs if m.id not in keep_ids] return {"messages": to_remove, "priority_buffer": buffer}

classify函数你可以用规则(关键词匹配)或小模型分类。规则版更快更稳,比如包含“必须”“不要”“目标”的判为 constraint,工具返回的判为 tool_result。

3.6 把四个策略组装进图

from langgraph.graph import StateGraph, END builder = StateGraph(AgentState) builder.add_node("window", sliding_window_node) builder.add_node("summarize", summarize_node) builder.add_node("recall", recall_node) builder.add_node("priority", priority_node) builder.add_node("agent", agent_node) # 你的 LLM 调用节点 builder.set_entry_point("priority") builder.add_edge("priority", "window") builder.add_edge("window", "summarize") builder.add_edge("summarize", "recall") builder.add_edge("recall", "agent") builder.add_edge("agent", END) graph = builder.compile()

顺序有讲究:先按优先级清理,再滑窗,再压缩,最后召回补充。召回放最后,是因为它注入的是“补充信息”,不应该被前面的清理逻辑误删。

4. 验证请求与成功结果:压测脚本与 token 曲线

配置写完不算完,得用数据证明策略有效。这一节给你一套可复制的压测流程。

4.1 构造长会话压测输入

模拟 30 轮工具调用对话,每轮包含一次工具返回的大块 JSON:

def build_stress_input(rounds=30): msgs = [SystemMessage(content="你是一个任务型 Agent,需要多轮调用工具完成目标。")] for i in range(rounds): msgs.append(HumanMessage(content=f"第{i+1}步:查询订单 {i} 的物流状态")) msgs.append(AIMessage(content=f"调用 query_logistics(order_id={i})")) fake_result = {"order_id": i, "status": "in_transit", "detail": "x" * 2000} # 模拟大块返回 msgs.append(SystemMessage(content=str(fake_result))) return msgs

4.2 记录 token 消耗

用tiktoken统计每次进入 agent 节点前的上下文长度:

import tiktoken enc = tiktoken.get_encoding("cl100k_base") def count_tokens(messages): return sum(len(enc.encode(m.content)) for m in messages)

在agent_node开头打印count_tokens(state["messages"]),跑完 30 轮,把每轮的数值存下来。

4.3 对照实验设计

跑三组:

组别策略预期
A 组无策略(纯追加)token 线性暴涨,后期报错或质量崩
B 组仅滑动窗口token 封顶,但可能丢关键约束
C 组四策略组合token 平稳,任务完成率高

每组用同一个 Model ID,通过 TaoToken 通道调用,记录三个指标:峰值 token、是否报 context 超限、最终任务是否完成。

4.4 成功结果长什么样

C 组跑下来,典型表现是:token 在第 8 轮左右达到平台期(约 4000-6000),之后不再增长;30 轮全部跑完无报错;最终 Agent 能正确复述“目标是查询全部订单物流”,说明摘要和召回保住了核心目标。

A 组通常在 15-20 轮之间触发context_length_exceeded,或者模型开始重复调用同一个工具——这是上下文污染导致决策循环的典型症状。

4.5 用模型对话页快速验证单点

如果你只想验证“某个策略节点是否按预期裁剪了消息”,不用跑全图。把裁剪后的 messages 拼成一段文本,贴到https://taotoken.net/models的对话页,看模型能否基于裁剪后的上下文正确回答。这比写断言快得多。

5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth

调度策略调通之前,通道类报错会先拦住你。这一节按真实报错逐个拆。

5.1 401 Unauthorized

最常见。原因三类:

  • Key 没读到:.env没加载,或变量名拼错。打印os.getenv("TAOTOKEN_API_KEY")[:8]确认。
  • Base URL 写错:必须是https://taotoken.net/api,不要多加/v1或漏掉协议头。
  • Key 失效或被删:去https://taotoken.net/api-keys重新生成。

5.2 local proxy failed / connection error

这个报错通常出现在你本地网络环境有额外转发配置时。排查顺序:先确认base_url拼写无误;再用curl直接打通道:

curl -X POST https://taotoken.net/api/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"你的ModelID","messages":[{"role":"user","content":"hi"}]}'

curl 通、Python 不通,就是代码层配置问题;curl 也不通,检查 Key 和地址。

5.3 reading choices 相关报错

典型信息是KeyError: 'choices'或reading 'choices' of undefined。这说明返回体结构和你预期的不一致,常见于:

  • Model ID 填错,通道返回了错误对象而非标准 completion。
  • 用了不兼容的调用方式(比如把 embedding 模型当 chat 模型调)。

解决:先打印原始resp,看返回体里到底有没有choices字段。Model ID 去https://taotoken.net/models核对。

5.4 OAuth / 鉴权头冲突

如果你同时装了多个模型客户端(比如 Claude Code、Cline),它们可能各自注入了鉴权头,导致请求头里有两个 Authorization。表现是间歇性 401。

排查:在代码里显式指定 headers,不要依赖全局环境。LangGraph 侧用ChatOpenAI的default_headers参数固定:

llm = ChatOpenAI( model=os.getenv("TAOTOKEN_MODEL"), api_key=os.getenv("TAOTOKEN_API_KEY"), base_url=os.getenv("TAOTOKEN_BASE_URL"), default_headers={"Authorization": f"Bearer {os.getenv('TAOTOKEN_API_KEY')}"}, )

5.5 三件套核对清单

无论你用 CC Switch、Cline MCP 还是 Codex 的auth.json,接入任何模型通道都逃不过三件套。以 Codex 的auth.json为例:

{ "base_url": "https://taotoken.net/api", "api_key": "sk-你的key", "model": "你的ModelID" }

Cline 的 MCP 配置同理,在 settings 里填 Base URL、API Key、Model ID 三项。CC Switch 切换配置时,确认这三项一起切,别只换 Key 不换地址。

5.6 策略层的“假成功”

还有一种隐蔽问题:不报错,但策略没生效。检查方法是在每个策略节点后打印len(state["messages"])。如果滑动窗口节点跑完消息数没降,说明RemoveMessage没被 reducer 正确处理——回去检查 State 的Annotated是否用了add_messages。

6. 语义一致 CTA:把对照实验跑起来

上下文工程的收益,只有在真实长会话里才看得出来。建议你按这个顺序推进:

先拿一个 Key,在https://taotoken.net/api-keys创建,把 Base URL 和 Model ID 填进.env。然后跑通第 2 节的最小验证脚本,确认通道没问题。接着把第 3 节的四个策略节点逐个加进你的图,每加一个就跑一次第 4 节的压测,记录 token 曲线。最后用不同 Model ID 做横向对比,找出“在你的业务场景下,哪个策略组合性价比最高”。

如果你主要做长期编码类 Agent,需要频繁切换模型做对照,可以看下 Coding Plan 的额度方案:https://taotoken.net/coding-plan。如果只是验证某个策略节点裁剪后的上下文质量,直接用模型对话页贴文本测试最快:https://taotoken.net/models。接入文档和参数细节在https://taotoken.net/doc,遇到鉴权问题先翻 API Keys 页面:https://taotoken.net/api-keys。

调度策略没有银弹。滑动窗口适合工具密集但历史弱的场景,摘要压缩适合长任务但怕丢约束,向量召回适合知识型检索,优先级队列适合目标驱动的多轮规划。真正的功夫在于:你知道自己的 Agent 在哪一轮开始“记忆过载”,以及那一轮到底丢了什么。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询