ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

用 LangGraph 破局!上下文工程四大高效调度策略,让 Agent 告别“记忆过载”

用 LangGraph 破局!上下文工程四大高效调度策略,让 Agent 告别“记忆过载” 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 URLOpenAI 兼容https://taotoken.net/api模型对话测试页https://taotoken.net/modelsLangGraph 本身不绑定模型它通过ChatOpenAI这类封装调用。因为 TaoToken 兼容 OpenAI 协议你只需要把base_url和api_key指过去model填 TaoToken 支持的 Model ID 即可。2.2 安装依赖pip install langgraph langchain-openai langchain-core tiktokentiktoken用来做 token 计数压测时统计上下文增长曲线别省这一步。2.3 环境变量配置不要硬编码 Key。建一个.envTAOTOKEN_API_KEYsk-你的key TAOTOKEN_BASE_URLhttps://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( modelos.getenv(TAOTOKEN_MODEL), api_keyos.getenv(TAOTOKEN_API_KEY), base_urlos.getenv(TAOTOKEN_BASE_URL), temperature0, ) resp llm.invoke(只回复两个字通了) print(resp.content)跑通再往下。如果这里报 401先解决鉴权别带着问题调调度策略否则你分不清是策略问题还是通道问题。2.5 为什么用统一通道做对照实验上下文工程的核心指标是“在固定 token 预算下任务完成率能到多少”。如果你每个模型用不同的 Key、不同的地址变量太多。统一到 TaoToken 后你的实验矩阵变成变量固定项变化项图结构四个策略节点不变调度策略滑动窗口/摘要/召回/队列逐个切换模型Base URL Key只换 Model ID指标token 消耗、成功率、延迟记录对比这样你才能说清楚“是策略起作用了还是换模型碰巧好了”。3. 可复制配置四大调度策略的 State 与 Node 片段这一节是核心。我把四种策略写成独立的 Node你可以按需组合进同一张图。所有片段都基于 LangGraph 的StateGraphState 用TypedDictAnnotated定义 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: intadd_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 提供了RemoveMessagefrom 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(idm.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(idm.id) for m in old] summary_msg SystemMessage(contentf[历史摘要] {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_keyos.getenv(TAOTOKEN_API_KEY), base_urlos.getenv(TAOTOKEN_BASE_URL), ) def recall_node(state: AgentState) - dict: query state[messages][-1].content # 假设 vectorstore 已在外层构建并持久化 docs vectorstore.similarity_search(query, k3) 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(keylambda x: x[level], reverseTrue) keep_ids {item[id] for item in buffer[:BUDGET]} to_remove [RemoveMessage(idm.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 轮工具调用对话每轮包含一次工具返回的大块 JSONdef build_stress_input(rounds30): msgs [SystemMessage(content你是一个任务型 Agent需要多轮调用工具完成目标。)] for i in range(rounds): msgs.append(HumanMessage(contentf第{i1}步查询订单 {i} 的物流状态)) msgs.append(AIMessage(contentf调用 query_logistics(order_id{i}))) fake_result {order_id: i, status: in_transit, detail: x * 2000} # 模拟大块返回 msgs.append(SystemMessage(contentstr(fake_result))) return msgs4.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( modelos.getenv(TAOTOKEN_MODEL), api_keyos.getenv(TAOTOKEN_API_KEY), base_urlos.getenv(TAOTOKEN_BASE_URL), default_headers{Authorization: fBearer {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 在哪一轮开始“记忆过载”以及那一轮到底丢了什么。
返回列表