拓冰建站拓冰建站
首页 / 资讯中心 / 正文

生成式AI设计模式第三篇:Agent工具调用与上下文管理实战解析

做生成式AI应用的时间越长越觉得这东西像搭积木。基础模型是积木本身但怎么搭、连接处怎么处理、塌了怎么补救才是决定项目能不能落地的关键。前面两篇我们聊了提示模板的基础模式和RAG检索增强模式这一篇继续往下走专门聊Agent场景下的工具调用和上下文管理。如果你正在做智能体、自动化工作流、或复杂对话系统这部分内容大概率能帮你少走几个月的弯路。我自己的体会是很多团队Demo做得飞快一到生产环境就崩问题基本都出在工具调用没有章法、上下文管理一团乱麻。生成式AI设计模式本质就是把那些反复踩坑之后沉淀下来的套路变成一套可以照抄的作业。这篇不会讲太多空泛的概念重点放在可落地的实现细节和踩坑记录上。1. 生成式AI设计模式体系回顾与第三篇定位1.1 为什么生成式AI也需要设计模式传统软件工程里的设计模式解决的是对象之间怎么协作、怎么解耦、怎么复用。生成式AI应用的开发表面上是在写Prompt、调API但实际面对的问题同样是协作和复用多个工具怎么被模型调度对话历史怎么管理记忆怎么分层输出怎么约束这些问题如果每次从零开始想项目必然失控。设计模式的价值在于它提供了一套已经验证过的解决套路。比如你遇到上下文太长第一反应可能是暴力截断但成熟的做法是滑动窗口加摘要压缩。这不是什么高深理论是很多团队用真实流量喂出来的经验。生成式AI应用迭代速度太快更需要这种模式化的沉淀否则每次需求一变就得重构一大片。另外设计模式也是一套通用的语言。你说“这里用RAG模式”对方马上明白你要做检索增强你说“工具调用走语义路由”对方知道模型不是写死调用哪个API。这种沟通成本上的节省在跨团队协作时特别明显。1.2 前两篇覆盖的内容与第三篇重点这个系列我们按场景层层递进。第一篇聊了提示工程的基础模式比如模板固化、少样本示例、输出格式约束解决的是“怎么让模型稳定输出”的问题。第二篇聊了RAG检索增强模式从文档切分、向量索引、召回排序到怎么把检索结果安全地塞进上下文解决的是“怎么让模型拥有外部知识”的问题。第三篇聚焦Agent场景。当模型不只是生成文字而是需要调用外部工具、读取文件、查询数据库、执行动作的时候整个系统的复杂度会再上一个台阶。这篇文章要拆解三个核心模式工具调用模式模型如何理解并调度真实函数上下文管理模式多轮对话中如何保住关键信息记忆分层模式短期记忆和长期记忆如何协同这三个点刚好是Agent开发中最容易翻车的地方。我会结合实际项目代码把每一步的思路和坑都摊开讲。2. 工具调用设计模式从语义路由到函数调用2.1 工具注册表与Function Calling机制现在主流的模型平台基本都支持 function calling也就是让模型根据你的工具描述输出一个结构化的调用请求而不是直接生成自然语言命令。这个机制的核心是工具注册表一个机器可读的函数列表每项包含函数名、描述、参数JSON Schema。我见过不少新手直接把所有工具一股脑丢给模型几十个函数塞进system prompt结果模型频繁选错、参数乱填、上下文还白白占掉一大截。正确的做法是先注册再筛选。把工具按业务域分类每个类出几个“代表工具”参与第一轮路由等模型确定大类后再加载该域下的具体函数。举一个实际例子。我们做内部运维助手时定义了大概三十多个工具如果全量注册光工具描述就占了四千多token。后来改成两级路由第一轮模型只选择“服务器操作”“日志查询”“数据库变更”等几个域第二轮再映射到具体工具准确率从68%直接跳到91%token开销还降了快一半。工具注册表的描述文本也有讲究。我踩过的坑是描述写得太模糊比如“获取用户信息”模型根本不知道要传什么参数。后来统一格式get_user_info: 根据用户ID或邮箱查询用户详细信息 参数: user_id (string, optional), email (string, optional) 注意: user_id 和 email 至少提供一个这样模型就能清晰判断该传什么、不该传什么。另外描述里不要出现“你可以”“请尝试”这类废话直接说功能和参数约束模型更认。2.2 语义路由让模型决定调用哪个工具工具调用的第一步不是直接设参而是让模型判断“现在该不该调用工具、调用哪个”。这个动作就是语义路由。实现方式有两种第一种把工具列表作为函数定义传给模型由模型在生成回复时决定返回一个或多个调用请求。第二种先用一次轻量级调用做意图分类命中后再走固定分支。第一种更灵活但存在不确定性第二种更稳但需要额外多一次模型调用。我个人的建议是对于工具数量少于十个的简单场景直接用第一种对于工具多、误调用成本高的场景切到第二种。做路由的时候还有个容易被忽略的点模型可能会在没有合适的工具时强行选一个。这种情况下一定要在指令里写清楚“如果无匹配工具回复UNKNOWN”。否则模型会为了完成用户请求而去调用一个八竿子打不着的工具轻则返回错误结果重则触发权限敏感的变更操作。2.3 参数解析与校验的边界模型返回的调用请求本质上是一段JSON但LLM生成的JSON偶尔会不合法。常见的是缺少逗号、布尔值大小写不对、字符串里残留注释。所以接住工具调用请求后第一步永远是容错解析而不是直接json.loads。我自己习惯的做法是两层保险先用正则或解析器把代码块外的杂质去掉。再用一个健壮的解析库比如Python的json5或demjson3。解析完成后必须做Schema校验。这一步很多人会省但省了迟早出事。模型生成的参数也许类型对但值超范围、或者出现不可能的组合。比如查询时间范围的接口start_time晚于end_time如果直接透传给数据库轻则报错重则全表查询。所以我在工具调用层加了一个轻量校验函数每个工具的Schema里允许配置自定义校验规则。校验失败时不直接报错而是把校验错误信息返回给模型让它重新生成参数。这一步对最终体验的提升非常明显——模型能自我纠错而不用让用户重新说一遍。2.4 调用失败重试与降级策略工具本身也会挂。网络超时、权限不足、依赖服务返回5xx这些在生产环境天天见。Agent系统里工具调用失败的处理方式和传统接口设计完全不同——不是简单的重试而是要把失败信息回传给模型让模型决定补救方案。比如用户要查订单订单系统超时。如果直接返回“系统错误”体验很差。更合理的链路是工具调用超时捕获异常。将错误信息包装为“tool_result_error”返回给模型并附带建议重试标志。模型判断是重试、还是换一个查询方式比如查缓存。我给工具调用加了一个统一的异常格式{ tool_name: query_order, status: error, error_code: TIMEOUT, error_message: 上游服务响应超时, suggestion: retry }模型读到suggestion后会生成“系统暂时繁忙我再试一次”的过渡话术同时尝试第二次调用。如果重试仍然失败就降级要么走缓存要么给用户一个明确的降级提示。这个机制极大减少了交互中的“硬失败”。3. 上下文管理设计模式窗口滑动与记忆分层3.1 上下文窗口压缩策略Agent和普通聊天机器人最大的不同是多轮对话里会穿插大量工具调用结果、中间状态、日志文本。这些内容如果不加控制三五个来回就能把上下文窗口撑爆。所以上下文管理必须是一个独立的模块而不是随手拼接字符串。第一招是滑动窗口裁剪。保留最近N轮的用户输入和助手输出更早的内容直接丢。但简单砍掉历史会丢失用户早期提到的关键信息比如用户第一轮说“帮我查一下上周的订单”后面一直在聊别的到第五轮如果你把第一轮砍了模型就不知道要查哪个订单了。第二招是摘要压缩。每隔几轮把前面的对话内容交给模型生成一段结构化的摘要包括用户目标、关键约束、已执行动作、当前状态。然后把原始对话替换成摘要再继续跑。这个做法的效果很稳定代价是每几轮会多一次模型调用。说实话这个成本值得花因为保住关键信息比省token重要得多。第三招是结构化存储。把对话中的实体和事实抽出来存成JSON字段查询时再放回去。比如用户提到“项目A的截止日期是周五”压缩时可以只保留这句事实而把其他无关讨论全部丢弃。3.2 长期记忆与短期记忆分离Agent系统里短期记忆是当前对话的上下文长期记忆则是跨会话持久化的用户画像和业务事实。两者如果混在一起很快整个上下文都会被历史信息占满当前任务反而被淹没。我的做法是把记忆分成三层会话层只存在于当前对话存轮次记录和临时状态。用户层跨会话的偏好和长期事实比如用户常说“回复尽量简洁”。业务层项目数据、外部系统同步来的关键实体状态。每次新会话启动不是把用户全部历史丢给模型而是先做一个记忆召回。从用户层和业务层挑出与当前请求相关的记忆片段转换成文本拼进system prompt。其他无关记忆继续留在存储里不占上下文。这个思想跟RAG是一样的不要试图把所有东西都塞进去而是按需检索。我见过最实用的做法是用向量库存长期记忆每次请求时用当前问题做向量相似度检索取top5记忆片段。效果比全量搬运好得多响应速度也快一大截。3.3 向量检索与缓存的混合使用长期记忆的召回离不开向量化但向量检索不是万能的。比如用户习惯说“还是老样子”这里的“老样子”完全无法通过相似度检索定位到历史订单。所以我会把检索和查询结合起来先用关键词粗筛候选再做向量精排最后再用一个轻量模型判断哪些片段真正相关。另外上下文管理的另一个重要模式是结果缓存。对生成式AI来说同样的请求重复命中时其实不需要重新调用大模型。尤其是那些工具调用结果比如用户连续几次查同一个接口第一次查完后把结果放进缓存后续直接返回既省时间又省钱。我自己习惯在三个层面做缓存接口层相同用户、相同上下文内容的请求直接返回历史回复。工具层相同参数的只读工具调用结果有效期几秒到几分钟。模型层对于确定性低的生成用低温度重试对确定性高的生成直接走缓存。缓存命中率一高整个系统的压力立刻降下来。但要注意缓存不能用在写操作和时效性敏感的场景必须用业务状态来控制失效时间。4. 实战一个多工具智能体的核心实现4.1 整体架构与运行流程纸上谈兵那么多接下来上一段可以直接参考的真实实现。我先说整体架构再贴关键代码。这里不绑定某个具体框架因为模式本身是通用的你可以用LangChain、Semantic Kernel甚至手写循环核心思路都一样。整个Agent的核心是一个循环接收用户输入。加上当前上下文系统提示、历史摘要、记忆召回组成Prompt。调用大模型得到回复或工具调用请求。如果是工具调用请求执行工具把结果追加到上下文回到第2步。如果是最终回复返回用户。这个循环看起来简单但每一步都有很多边界条件要处理。下面我会暴露一些关键实现。4.2 工具注册与调用的核心代码先看工具注册的Python示例。这部分我特意写得精简突出模式而不是框架细节。from typing import Any, Dict, List import json class ToolRegistry: 工具注册表管理所有可调用工具 def __init__(self): self.tools: Dict[str, Dict[str, Any]] {} def register(self, name: str, description: str, parameters: dict, func): self.tools[name] { name: name, description: description, parameters: parameters, func: func, } def list_tool_schemas(self) - List[dict]: 生成发给模型的工具描述列表 return [ { type: function, function: { name: t[name], description: t[description], parameters: t[parameters], } } for t in self.tools.values() ] def call(self, name: str, arguments: Dict[str, Any]): tool self.tools.get(name) if not tool: raise KeyError(f未知工具: {name}) return tool[func](**arguments)然后是这个主循环。这里的重点在错误回传和迭代上限这是防止Agent跑飞的关键。def agent_loop(user_input, registry, memory, max_iterations8): context memory.build_context(user_input) for i in range(max_iterations): response call_model([ {role: system, content: SYSTEM_PROMPT}, {role: user, content: context} ], toolsregistry.list_tool_schemas()) if response.tool_calls: tool_name response.tool_calls[0].name args response.tool_calls[0].arguments try: # 关键容错解析 args json.loads(args) result registry.call(tool_name, args) context f\n工具调用结果: {json.dumps(result, ensure_asciiFalse)} except Exception as e: # 关键错误信息回传给模型让它自己修正 context f\n工具调用错误: {e} continue else: return response.content return 抱歉我尝试了多次仍无法处理该请求请稍后再试。这段代码看着简单已经包含了工具调用模式最核心的三个点工具注册、容错解析、错误回传。实际项目里还会加鉴权、审计、超时控制但骨架是一样的。4.3 上下文压缩器实现细节接下来是上下文管理的关键模块上下文压缩器。它的作用是控制上下文长度同时保留重要信息。def compact_context(long_context: str, max_tokens: int 3000) - str: 当上下文超长时用摘要压缩替代原始内容 prompt ( 请将以下对话历史压缩为结构化摘要保留用户目标、 关键约束、已执行动作、当前状态。不要丢失关键事实。\n\n f历史内容:\n{long_context} ) summary call_model([ {role: user, content: prompt} ], temperature0.1, max_tokensmax_tokens) return summary这个函数的调用时机需要设计好。我一般会在Agent循环内统计当前上下文的token数超过阈值时才触发。阈值不是固定值按模型上下文窗口的一半来算比如8k窗口就设4k16k窗口就设8k留出足够的余量给后续新内容和新回复。压缩后原始对话历史会被替换成摘要。但要注意摘要压缩本身也会丢失细节所以我额外维护了一个memory对象把关键实体和关系同步存储压缩时再补回摘要里。这个做法虽然多费一点逻辑但能显著减少信息丢失的概率。4.4 记忆分层的落地实现记忆分层的代码并不复杂难点在设计存储结构。我习惯用三张表或者三个字典来存class Memory: def __init__(self): self.session {} # 会话层 self.user {} # 用户层存偏好 self.business {} # 业务层存实体状态 def save_session(self, key, value): self.session[key] value def save_user(self, key, value): self.user[key] value def recall(self, query: str, k: int 5) - str: candidates [] # 从所有层召回候选 candidates [f[用户] {v} for v in self.user.values()] candidates [f[业务] {v} for v in self.business.values()] # 简单关键词匹配打分实际可以用向量检索 scores [sum(q in c for q in query.split()) for c in candidates] top_idx sorted(range(len(scores)), keylambda i: scores[i], reverseTrue)[:k] return \n.join([candidates[i] for i in top_idx if scores[i] 0])这个简版用一个关键问题是想说明记忆召回不必一上来就上向量库很多时候关键词匹配已经够用。你把用户偏好、业务实体挨个存好每次按当前问题挑几个出来比全量塞给模型强得多。等到业务复杂了再替换成向量召回但召回前的过滤逻辑是一样的。5. 常见问题与排查技巧实录5.1 工具幻觉与循环调用Agent开发最经典的问题是模型明明没有合适的工具可用却编造出一个不存在的函数或者强行调用一个参数完全对不上的函数。这本质上是模型的幻觉。我的排查经验是先看工具描述是否过于泛泛再看是否设置了“UNKNOWN”兜底。如果都做了还出现幻觉那就是温度参数的问题把temperature调到0.1左右幻觉概率会明显下降。循环调用是另一个大坑。比如模型调用A工具结果A返回错误模型又调用A反复横跳。我最快的一次线上事故就是Agent卡在循环里几分钟白白烧了几百次调用。解决办法就是迭代上限同时记录每次工具调用的历史和失败原因如果是同一工具连续失败超过两次就强制切换策略不再让模型继续重试。5.2 上下文溢出的三种表现上下文溢出不止是报length exceeded还经常表现为生成质量突然变差、工具调用忘带参数、答非所问。这些都是上下文太长导致模型的注意力被稀释。我的排查顺序是先看token用量统计再看上下文里有没有塞入过长的工具返回结果。关键优化点在于工具结果摘要。很多工具的返回JSON动辄几千行不能直接丢给模型。正确做法是对工具结果做字段裁剪、分页摘要或结构化摘要只保留模型回答用户问题所需的最小信息。比如查询订单列表只要返回订单号、状态、金额和时间其他详情一律不塞入上下文。5.3 可观测性与日志回放Agent系统调试难度比传统接口高得多因为每一步模型决策都是概率性的。你会发现上次跑通了这次同样的输入却失败了。所以可观测性必须从第一行代码就开始做。我每次都会记录完整的调用链日志输入文本模型原始输出解析后的工具调用请求工具执行结果上下文截断后的长度每轮耗时和token数有了这些日志问题回溯基本就是打开日志看一遍的事。我甚至会把每次Agent运行的完整轨迹导出成HTML文件方便团队成员直接在一个页面里看到每一步的前后因果。这个投入非常值得能节省大量排查时间。5.4 测试与回归评估Agent的测试不能只盯着单轮输出的对错要设计任务级测试集每个任务包含多轮交互和工具调用。比如“用户要查订单然后取消其中一个”这个任务至少准备十几种变体覆盖订单不存在、取消超时、权限不足等边界情况。回归测试跑完后需要关注两个指标任务完成率和工具调用准确率。前者看最终用户目标是否达成后者看模型有没有调用错误的工具。只把模型参数调一版这两个指标可能会一升一降这时候要结合业务场景取舍。我自己很看重完成率因为它直接影响用户体验。测试还可以考虑加入“对抗性输入”比如用户故意把几个工具串在一起发出模糊指令。这类输入最能暴露路由和上下文管理的薄弱点值得在测试集里专门维护一个目录。最后分享一点我的实际体会说实话生成式AI设计模式这个话题真正难的不是某个模式本身而是把模式组合起来的时候怎么权衡。工具调用、上下文管理、记忆分层每个单独拆开都讲得明白一旦叠加在一起约束就变得复杂起来。比如你为了缩短上下文做了摘要压缩结果摘要里丢掉了上次工具调用结果的细节下一轮模型就没办法根据细节做正确判断。这种“按下葫芦浮起瓢”的体验我经历得太多。后来我想明白一件事设计模式不是用来减少问题数量的而是把问题限制在可预测范围内。你用迭代上限防止死循环用容错解析防止JSON崩掉用记忆分层防止信息丢失。这些手段会让你付出一些额外的开发成本和模型调用成本但换来的是系统的可控性。对一个要上线的Agent来说可控比聪明重要得多。如果你正在做自己的Agent项目我建议不用一上来就追求最全的模式。先跑通一个最简单的循环然后盯住日志里反复出现的那类错误再针对性地加一个模式。这样一步步演进比你照着某篇文章抄一遍要好使得多。生成式AI的变化太快唯一能长期依赖的就是那种随时能根据实际问题做取舍的能力。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门