资讯动态

联网多轮Agent开发实战:上下文窗口与Token预算的巧妙控制

发布时间:2026/10/21 5:03:01 来源:尧图企业网站定制
1. 为什么Agent非要联网不可模型知识固化的局限先从一个尴尬的场景说起。我本地部署了一个开源大模型问它“今天上海天气怎么样”它一本正经地告诉我今天晴转多云然后我在阳台被雨淋了个透。又试了一次“最近发布的某款新手机起售价是多少”它开始编参数甚至把上一代的配置搬了出来。问题出在哪儿模型的知识是有截止日期的。绝大多数模型的训练数据停在某个时间点之后发生的事情对它来说完全是盲区。这件事放在单轮问答里还能忍一旦牵涉到Agent场景就完全不行了。Agent的核心价值是“替用户执行任务”而任务往往依赖实时信息查天气、盯股价、比价、查航班、抢票、找最新的技术文档。如果Agent只能靠记忆作答它就不是Agent是个复读机。所以“联网”这两个字是Agent从玩具走向工具的分水岭。联网之后又冒出两个连锁问题第一Agent是一次性问答还是多轮对话用户不可能一句话把需求说完必然会追问、修正、反悔。多轮对话意味着Agent要记住上下文而上下文里又混入了联网抓回来的网页内容这些东西都占token。第二token怎么算、怎么控、怎么省这不是省钱的问题是能不能跑得下去的问题。上下文窗口有限联网内容又多聊几句就爆窗口的Agent我见过太多。所以这篇我把整个链路拆开讲联网检索怎么接、多轮状态怎么维持、token怎么精打细算最后给一个能直接跑的Demo。适合两类人一是刚接触Agent开发、想把“调用API”升级成“能干活”的开发者二是已经在做Agent、但被多轮对话和上下文管理折磨到头疼的从业者。2. 联网Agent的整体架构工具调用是骨架搜索管线是血肉2.1 先理解“工具调用”在Agent里的位置Agent要联网不是直接把网页内容塞给模型——第一步是让模型“知道”它可以去网上查。这件事在实践里主要有两种做法。一种是“提示词硬控法”你在系统提示词里写清楚“当用户问实时信息时你需要调用search工具查询”然后把搜索接口的调用方法告诉模型让模型在回复内容里输出一个特定格式的指令比如[[SEARCH:今天上海天气]]。代码层拿到这个指令就去调搜索接口把结果拼接回上下文再重新请求模型。这种方法实现简单、透明可控适合原型验证但缺点也很明显模型容易漏触发或多触发指令格式稍微错一点就匹配不上。另一种是官方标准的“Function Calling / Tool Calling”机制。模型在推理时会额外输出一个结构化指令通常是JSON内容类似“我想调用search工具参数是{query: 今天上海天气, city: 上海}”由代码层解析后执行真实工具再把工具返回值送回模型。这个机制的好处是参数结构化、可靠度高主流模型和Agent框架都支持已经成了行业事实标准。实操上我更推荐后者。不要自己发明协议除非你想维护一套和全世界都没法兼容的接口。2.2 从“搜到”到“用上”完整链路拆解联网检索看着简单其实是一个三段式管线搜索阶段调用搜索接口类比你自己打开浏览器输入关键词。搜索接口返回的是条目列表每条包含标题、链接、摘要。注意这一步返回的信息密度不一定够光是摘要往往喂不饱模型。抓取阶段选定前几条结果抓取网页正文内容。这里有个问题很多页面反爬或者结构杂乱直接抓回来一堆导航、广告、弹窗脚本。实践中需要用正文提取工具做内容清洗。消化阶段把清洗后的正文按token预算截断注入上下文让模型作为参考资料进行总结和回答。这三段各有各的坑。搜索阶段的问题是关键词生成质量——模型生成的搜索词可能太口语化或太宽泛抓取阶段的问题是内容噪声大消化阶段的问题是token占用高。后面我会给出每个环节的应对策略。2.3 一个能“记住”的Agent需要什么从无状态到有状态单轮联网问答只需要“拿搜索词—调接口—拼上下文—出结果”四个步骤。但多轮对话让它复杂了至少三倍用户在第2轮说“还是搜一下A吧”模型得知道“A”指的是上一轮说的某件事用户在第3轮说“那B呢”模型得把B和前面的话题关联起来用户在第5轮问“刚才那个价格是多少来着”这要求系统至少保留前几轮的问答内容。也就是说多轮Agent必须有一个“状态容器”要么自己管理历史消息对象要么依赖框架的Memory机制。而联网结果要不要进这个容器我见过两种做法一种是临时注入——搜索结果只在本轮使用结束后丢弃另一种是永久归档——把搜索结果提炼成摘要塞回历史消息。选择没有绝对对错关键看你产品的定位如果用户回头问“你刚才查到的那个数据是什么”临时注入就答不上来如果用户只是随口追问现场消息永久归档又会白白占用大量token。多数产品会折中保留“搜索摘要”而不保留“原始网页全文”。3. 多轮对话的上下文管理会话窗口、记忆裁剪与注入策略3.1 三层会话结构别把所有消息塞进一个篮子我建议把Agent的对话状态拆成三层别让所有消息混在一起。这套结构在多轮联网场景里非常实用系统层System固定指令、角色设定、工具定义、行为约束。每次请求都会带但它不应该频繁变动也不应该膨胀。历史层History用户和助手的对话记录保持原始形式或经过压缩的摘要。当轮上下文Current Turn本轮用户输入、联网检索出来的临时结果、系统内部运行日志比如调用了哪个工具、返回了什么状态。为什么要分三层因为三层的生命周期不同系统层是常量历史层是累计变量当轮上下文是临时变量。把临时变量塞进历史层是我见过最多的内存泄漏——每轮聊完都把搜索结果原样塞进历史聊到第8轮历史里躺着6大段网页正文窗口直接爆掉。3.2 上下文窗口的预算分配公式当你把一条请求发出去它实际吃掉的是总输入tokens 系统提示词 tokens 历史消息 tokens 当前输入 tokens 联网结果 tokens假设你的模型上下文窗口是128K我的建议是给“当前用户输入联网结果”留足40-50%的预算给系统提示词和工具定义留10-15%剩下的给历史消息。因为本轮任务是当前最需要上下文空间的历史可以压缩但本轮的搜索结果往往决定回答质量。这里面有个反直觉的地方很多人的token爆炸不是模型吃得多而是历史层攒得太肥。用户信息量其实不大但Agent每轮回答都很啰嗦模型为了凑长度生成大段废话几十轮下来历史全是空气。所以控制历史层不只靠裁剪还得靠“回答质量规范”——让模型尽量精简作答省下来的窗口全是红利。3.3 三种历史裁剪方案截断、摘要、滑动窗口多轮对话聊天记录怎么处理无非三种思路各有适用场景截断只保留最近N轮更早的丢掉。最简单粗暴适合闲聊型Agent。损失是跨多轮的长期信息会丢比如第2轮聊过的需求第15轮可能会被提起。滑动窗口定长窗口新的进来旧的出去。本质上还是截断但实现上更优雅适合固定预算场景。社区里常用deque(maxlenN)来做代价同样是不能恢复遥远记忆。摘要压缩用模型把早期对话提炼成一段摘要替换原始消息。适合长会话、强记忆场景。代价是多一次模型调用、多一笔token开销而且摘要会丢失细节。我个人实践中最稳的组合是平时用“截断保留最近10轮完整记录”当历史长度超过阈值时把更早的部分压缩成摘要。这样既保留最近细节又不会让远古信息彻底消失。注意压缩摘要本身也占token一般控制在200-500字以内否则得不偿失。3.4 非对称裁剪记住“承诺”和“数字”忘掉“寒暄”更进一步我建议做“结构感知”的裁剪——不是机械地按轮次切而是按信息类型保。多轮对话中的信息价值差异极大“明天下午三点开会”这种具体约定丢了会误事“好的呢”“明白了”这种客套或者确认丢了毫无影响。我自己会让Agent在每轮回答时顺带输出一个“记忆标记”属于内部运行数据不进用户可见输出标记当前轮里哪些是“需要长期记住”的信息姓名、数字、日期、偏好哪些是“会话级”信息。裁剪时优先丢会话级信息保住承诺和数字。这套机制的效果远超无脑滑窗。4. tokens的计量、成本泄漏点与预算控制4.1 tokenizer不只是“切词”它决定你花的每一分钱很多初学者不理解token到底怎么算的我打个比方tokenizer是模型理解文本的“基本单元”窗口英文一个单词通常1-2个token中文一个字通常1个token不同模型可能略有差异有的模型中文按字切、有的按词切还有的会对重复字符做特殊压缩。你的“预算”本质就是往这个窗口里塞多少切好的单元。同一个提问中英文、繁简体、不同表达方式的token数可以差两三倍。我自己有段时间把提示词从英文改成中文同样的逻辑描述token直接少掉近一半。所以做Agent的第一个省钱技巧就藏在语言选择里——前提是你的模型对中文支持够好。4.2 四个隐蔽的token泄漏点实操里我发现token消耗总是比预期高罪魁祸首通常是以下四个地方每个我都踩过一是系统提示词膨胀。一开始写了5条规则后来加了3个例子又追加2条边界说明再塞一个few-shot示例。不知不觉系统提示词从500字涨到3000字。模型每次请求都要重读这一大段这是固定成本、刚性支出。所以系统提示词要设计成“增量式”基础版一套特殊场景追加模块别把所有规则常驻。二是工具定义过长。Function Calling机制需要把每个工具的名称、描述、参数Schema发给模型这部分也算token。如果你定义了20个工具每个描述写200字光工具定义就是4000字。实战建议只定义会真实用到的工具描述写到“语义够用就行”别写论文。三是联网结果的整段塞入。搜索接口返回5条结果每条摘要400字再加上抓取的网页正文3000字一次联网就吃掉5000字。如果你每轮都联网每轮都塞成本直接爆炸。办法是只保留前三名的结果并用摘要替代正文——除非任务明确要求原文精确数据否则不要让模型看网页全文。四是历史消息重复计费。你每发一次请求历史消息都要完整重发一遍。50轮对话第50轮的请求里包含了前49轮的全部文本。这个在API计费体系下是照单全收的没有“增量计费”这种好事。所以每多一轮成本往上叠一层。没用的寒暄要主动压缩掉早压缩早省钱。4.3 预算控制三板斧上限、阈值、熔断要防止token失控光靠节省不够得有硬性机制设置单轮上限比如每轮联网结果的token上限为2500超了就截断历史层上限为8000超了就压缩或裁断。系统提示词上限为2000。设置会话总预算比如一个会话最多消耗5万token包括输入输出到达4.5万时提醒用户“快超了”到5万就拒绝继续。客户对话、付费场景特别需要这个。熔断机制如果某轮联网返回的内容特别大比如抓到一个2万字的网页立刻丢弃而不是截断——因为截断后的乱七八糟内容还不如不读。有一个细节需要注意输出tokens同样收费而且模型有时会复读、会反复生成“好的我来帮你查询”这样的寒暄。会话里设置max_tokens能限制单次输出长度但完全抑制啰嗦还得靠提示词和温度参数配合。5. 实操手写一个可运行的“联网多轮对话Agent”Demo5.1 架构选型没必要上来就上重型框架很多人一上来就引入一堆Agent框架结果光安装依赖就折腾一天。做原型阶段我建议最多用一个轻量级工具或者干脆手写。下面这个Demo依赖极简可以本机直接跑通requests调搜索接口和模型接口json解析结构化输出urllib.parse拼接URL模型侧任意提供Chat Completion接口的模型服务我用MaaS_URL和MODEL_NAME占位你替换成自己实际使用的即可核心数据结构就一个conversation_list用列表存历史消息每轮对话后追加用户消息和助手消息。这样最小可用后续维护也不费劲。5.2 代码骨架检索、上下文组装、多轮循环import requests import json # 占位参数换成你自己的真实配置 MaaS_URL https://your-llm-api.example.com/v1/chat/completions MODEL_NAME your-multimodal-model # 以你实际使用的模型为准 API_KEY your-api-key SEARCH_API https://your-search-api.example.com/search # 系统提示词既定义联网工具也要求精简输出 SYSTEM_PROMPT 你是联网助手。当用户需要实时信息时调用search工具。 搜索关键词要提取核心实体避免冗余。 回答时先给出结论再给来源。不要输出“好的”“我来看看”这类客套。 工具search。调用方式在回答前输出JSON {\action\: \search\, \query\: \关键词\} conversation_list [ {role: system, content: SYSTEM_PROMPT}, ] def call_llm(messages): payload { model: MODEL_NAME, messages: messages, temperature: 0.3, max_tokens: 512, } headers {Authorization: fBearer {API_KEY}, Content-Type: application/json} resp requests.post(MaaS_URL, headersheaders, jsonpayload, timeout60) resp.raise_for_status() return resp.json()[choices][0][message][content] def search_web(query): params {q: query, count: 3, language: zh} r requests.get(SEARCH_API, paramsparams, timeout30) results r.json().get(items, []) res_text [] for item in results: title item.get(title, ) snippet item.get(snippet, ) link item.get(link, ) res_text.append(f标题{title}\n摘要{snippet}\n来源{link}) summary \n\n.join(res_text) # 预算控制搜索结果超过2000字就截断 return summary[:2000] def run_agent(user_input): # 加用户消息 conversation_list.append({role: user, content: user_input}) # 第一轮让模型判断是否需要搜索 reply call_llm(conversation_list) # 尝试从回复头部解析出search工具调用 action None try: if reply.startswith({): parsed json.loads(reply) if parsed.get(action) search: action parsed[query] except Exception: action None if action: # 执行搜索 search_res search_web(action) # 把搜索结果作为系统消息插入然后又跑一轮模型 tool_context { role: system, content: f以下是联网搜索结果回答时优先参考\n{search_res}, } tmp_messages conversation_list [tool_context] final_reply call_llm(tmp_messages) # 清理掉临时的联网上下文避免历史膨胀 conversation_list.append({role: assistant, content: final_reply}) return final_reply else: conversation_list.append({role: assistant, content: reply}) return reply # 五轮对话测试 while True: user_input input(你) if user_input.lower() in (quit, exit): break output run_agent(user_input) print(助手, output)这段代码逻辑很直白先让模型决定要不要搜索要搜就调接口把结果临时塞进上下文再跑一次最后把临时内容扔掉、只保留正式回复。这里有个巧妙的设计联网结果是作为“临时system消息”注入的而不是追加进历史列表这样就不会污染多轮记忆。5.3 复现时最容易翻车的5个点搜索结果字段名不一样。不同的搜索接口返回结构不同有的叫items有的叫results有的没有link字段。写代码前先打印一段原始返回值看看别想当然。Action解析失败。模型输出的JSON不总是标准JSON经常夹杂前后解释。前面示例代码只做简单判断实际上线建议用正则提取{.*?}块再交给JSON解析。更稳妥的走Function Calling机制让框架来解析。模型上下文不兼容。有些模型不支持中文系统提示词里的“不要客套”指令照样输出“好的让我帮你查一下”。这种调几次就发现模型性格改不了要么换更强的模型要么后端做一层输出清洗。多轮会话里用户指代歧义。第3轮问“它多少钱”模型如果看不到第2轮的上下文就懵了。这正是为什么历史消息不能丢。但历史消息过长又会爆窗口——所以就有了递归摘要的需求。在上面的Demo里第10轮之后建议把早期消息替换成一段摘要。联网接口超时。搜索接口偶尔要3-5秒加上模型推理时间整个响应可能过10秒。客户端要有耐心提示后端要有超时重试否则用户体验极差。5.4 给Demo加一个轻量级“记忆压缩”模块正式做产品时上面的Demo还不够。我再给一段记忆压缩的扩展def compress_history(conversation_list, max_rounds8): # 系统消息保持独立其余消息按轮打包压缩 sys_msgs [m for m in conversation_list if m[role] system] newest_msgs conversation_list[- max_rounds * 2:] # userassistant各一条算一轮 old_msgs conversation_list[1:- max_rounds * 2] if max_rounds 0 else conversation_list[1:] if len(old_msgs) 2: return conversation_list # 历史还短不压缩 # 让模型把旧历史压成一段摘要 compress_input { role: user, content: f把下面对话压缩成200字以内的摘要保留所有数字、名字、约定\n{json.dumps(old_msgs, ensure_asciiFalse)}, } summary call_llm(sys_msgs [compress_input]) compressed_placeholder {role: system, content: f[早期对话摘要] {summary}} return sys_msgs [compressed_placeholder] newest_msgs这段代码会在历史轮数超标时触发压缩旧消息变成摘要新消息完整保留。注意摘要里“保留所有数字、名字、约定”这个要求很重要否则模型会不负责任地吞掉关键信息。6. 我踩过的坑联网内容可信度、延迟和上下文污染6.1 搜索结果是错的模型照样会“照单全收”最危险的一次用户问某产品价格搜索接口返回了一条去年旧闻模型不管时间戳直接引用报价。用户火冒三丈。这个锅其实一半在搜索接口——返回结果没有按时效性排序另一半在Agent——没做“信息时效校验”。我现在的做法是搜索返回后先让模型判断一下“这条信息是否可能过期”通过时间描述、事件描述等信号标记为不确定的信息必须显式告知用户“时效性未知”。别怕多花一轮token这种可靠性投入值回票价。6.2 延迟叠加搜索模型推理可能突破用户的耐心阈值一个多轮联网Agent的完整耗时是模型判断是否搜索1-2秒 搜索接口2-5秒 网页抓取1-3秒 模型综合回答2-4秒。加起来经常超过10秒。我的改进办法搜索接口用异步“预查询”先不急着让模型决定只要检测到用户消息里包含明确的实体词如“天气”“价格”“最新”就预先把搜索结果准备好等模型说需要时直接给它。抓取阶段只抓前1条正文其余靠搜索摘要省掉大部分抓取时间。流式输出SSE让用户先看到“正在生成”而不是干等。这不算作弊这叫体验设计。6.3 上下文污染联网内容混入多轮记忆前面Demo里我特意强调把联网结果做成“临时注入”。这一点在真实项目里反复验证过一旦把网页原文直接追加进历史列表后面任何一轮对话都会受到这份“旧网页”的影响。比如用户第2轮查了天气第8轮问“推荐一下电影”模型竟然还在拿“今天有雨”说事。这不是模型笨是你把仓库里过期的蔬菜一直摆在冰箱前面。所以规则很简单网页内容是一次性消耗品用完就扔只有模型的最终回答可以作为历史保留。6.4 模型“假联网”它会编造搜索结果这也是个经典坑。你以为模型调用了搜索接口其实它只是在扮演一个“会搜索的助手”被问“最近有什么新闻”时直接自己编新闻。隐患在家里如果模型压根没有走工具调用路径你压根没法拦截和审核内容。我的补救是给回答加上“来源编号”强制要求模型必须引用搜索结果条目编号比如[1]代码层再校验编号对应的链接真的存在于本次搜索结果里。编造的引用会暴露无遗。这比提示词管用得多。7. 最后的几点实操建议把联网、多轮对话和token管理揉在一起本质上是在做三件事让模型接上活水实时数据、让模型记住上下文历史状态、让模型在预算内干活token控制。这三件事没有哪个可以省也没有哪件是一劳永逸的。根据我自己的经验给几条直接可以抄作业的建议别急着上重型框架先用最小可用版本跑通你的核心场景。框架解决的是通用问题你的场景可能根本需要不同的信息处理链路。跑通以后框架才有意义。给每个联网Agent先定义“超时预算”和“token预算”再开始写代码。这个话题下预算不是上线前的优化项而是需求文档的一部分。先定了预算架构就会自然倾向“精简”很多失控都会被反向遏制。把搜索质量和内容清洗当成一等公民。很多Agent效果差不是模型不行是喂给它的网页内容太脏。花时间调正文提取远比换一个更大的模型更划算。多轮对话里把“压缩摘要”当成一个持续动作而不是事后的补救措施。每次回复之后哪怕历史不太长也可以顺手生成一个“记忆要点”存起来后面真要压缩时直接拼接要点不需要重新读一遍全部历史。做这类Agent最深的体会是模型的能力天花板已经在哪儿摆着了真正拉开差距的是工程侧的数据管线、上下文管理和预算设计。联网不是给模型加一个搜索按钮那么简单它是一个完整的实时信息链路。把这套链路跑顺了你的Agent才真正开始“活”起来不然永远是玩具。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑