资讯动态

AI Agent最小闭环实战:单Agent工具调用与上下文管理

发布时间:2026/10/23 5:37:44 来源:尧图企业网站定制
做 AI Agent 这件事最容易被带偏的地方不是技术难度而是起步姿势。我见过太多人一上来就选一个功能齐全的编排框架装完依赖、看完几十页文档、配好一堆抽象层然后卡在“第一个能跑通的 demo”上两周过去连一次完整的工具调用都没闭环。所以当我准备做一系列 AI Agent 小项目时第一条原则就是先做一个“小到能在一晚上跑通、又完整到能直接改造成生产雏形”的智能体。这篇就聊聊我在这个方向上踩过的坑、做过的取舍以及一套可以直接参考复现的实现路径。所谓“小项目”指的不是玩具而是把大模型、工具调用、循环控制、上下文管理这几个关键部件压缩到最小可运行单元让整条链路清晰可见——你随时能指着某一行动手改而不是在一堆隐藏的抽象层里猜它到底干了什么。适合正在观望 Agent 怎么落地的人也适合已经用过现成产品、想自己拆开看看里面长什么样的开发者。我会从整体设计思路一直讲到具体的代码骨架、参数选择、成本核算和排障清单中间穿插一些只有真跑过才会知道的细节。1. 从“能不能跑通”倒推项目边界1.1 为什么我坚持先做单 Agent 而不是多智能体新手最容易犯的错是一上手就设计一个“研究员 程序员 审核员”的多智能体协作系统。听起来很美实际跑起来你会同时面对三个以上不可控变量谁在什么时候说话、消息怎么路由、失败怎么回滚、成本怎么归因。一旦结果不对你连是哪个环节出问题都定位不了。单 Agent 的价值就在于它是可观测的。整个系统只有一个决策者一次循环里的“思考—行动—观察”三步是串行的日志一打出来你立刻能看清它为什么调这个工具、参数从哪来、返回了什么、下一步为什么这么走。这个可观测性在调试阶段的价值远大于多智能体带来的那点结构美感。我的建议是把项目切成清晰的阶段第一阶段只做单 Agent 加两三个工具跑通闭环第二阶段加记忆和结构化输出第三阶段才考虑多 Agent 或复杂编排。每一阶段结束都有一个“能用的东西”而不是攒一个永远不完整的宏大架构。1.2 一个最小 Agent 到底由哪几块组成抛开各种框架的包装一个能干活的 Agent 就是四件事的组合我用一个生活化的类比说明大脑大模型本身负责理解和决策。它不存记忆每次都是“失忆状态”下看一遍你给的完整上下文。手脚一组可调用的函数也就是工具。模型不能真的执行任何操作它只能输出“我想调用某某工具参数是这些”真正的执行由你的代码完成。记事本上下文窗口。里面装着系统提示词、历史对话、每次工具调用的结果。这个记事本是有限的而且是唯一的信息来源。循环控制器你自己写的调度逻辑。判断模型是想继续干活还是已经给出最终答案决定循环几次、什么时候强制收尾。很多人觉得 Agent 神秘是因为把它当成了一个“会自己思考的程序”。其实它就是一个 while 循环把上下文喂给模型模型返回一个动作执行动作把结果追加进上下文再喂一遍。所谓智能来自模型对上下文的解读能力所谓工程全部集中在你怎么组织这个上下文、怎么定义工具、怎么控制循环。1.3 框架选型三个层级的取舍工具选型这块我试过三条路线各自的适用场景差别挺大路线代表形态上手成本可控性适合谁原生 API 裸写直接调对话接口自己写循环半天极高每一行都看得见想彻底搞懂原理、需要深度定制轻量编排库提供工具注册、循环封装一到两天较高出问题能钻进源码大多数业务场景全功能框架内置大量抽象、可视化编排三到五天较低异常常藏在抽象层里团队协作、快速原型验证我个人的选择是第一个项目用原生裸写第二个项目再用轻量编排库。理由很直接——你不先用裸写跑一遍就看不懂轻量库在帮你做什么等你看懂了用库才用得安心出了问题也知道往哪查。至于全功能框架它更适合已经明确需求、需要多人协作的阶段拿它做学习入口反而增加认知负担。提示如果你所在团队对可维护性要求高哪怕选框架也一定要确保它能让你看到“最终发给模型的那段完整上下文”。看不到这一层后面所有排障都是盲猜。1.4 项目定位先选一个“结果可验证”的任务小项目的任务选择非常关键我踩过最亏的一次坑是拿“帮我写行业分析”当第一个 Agent 任务。问题在于结果没法自动验证——你不知道它写得好不好不知道是提示词的锅还是模型的锅调优完全靠感觉最后陷入无休止的玄学调整。后来我把任务换成资料整理给定一批本地文本文件让 Agent 自己去检索、摘取关键信息、按固定结构输出一份摘要。这个任务的好处是结果有明确结构对错可以肉眼判断甚至写脚本校验工具调用路径短通常两到四轮就能收敛出错时能清楚区分“检索错了”还是“总结错了”。对练手来说这种“可验证的窄任务”比“开放式的宽任务”有价值得多。2. 四个核心部件的落地细节2.1 系统提示词把“人设”写成“操作规程”新手写系统提示词容易写成岗位描述“你是一位资深分析师擅长……”。这类写法对 Agent 帮助很有限因为模型不知道具体该怎么做。Agent 的系统提示词本质是一份操作规程要写清楚四件事任务目标、可用工具及其使用时机、输出格式、遇到异常怎么办。我常用的结构大致是这样你是资料整理助手任务是根据用户问题从给定资料中提取信息并输出结构化摘要。 可用工具 1. search_docs(query) —— 在资料库中检索相关段落。当你不确定答案所在位置时调用。 2. read_section(doc_id, section) —— 读取指定文档的某一节完整内容。当检索结果片段不足时调用。 工作流程 - 先检索再判断片段是否足够回答不足则继续检索或读取完整章节。 - 最多检索 4 次超过后必须基于已有信息作答。 输出格式 严格输出 JSON字段为 summary字符串、evidence字符串数组、confidence0-1 之间的小数。 异常处理 - 若检索结果为空直接返回 summary 为“未在资料中找到相关内容”confidence 为 0。 - 不要编造资料中不存在的事实。这里每一段都有明确用途。“工作流程”这一节极其重要因为它把原本需要模型自由发挥的规划过程收敛成了一条固定路径大幅降低了跑偏概率。“最多检索 4 次”这种硬约束是防止后面要讲的死循环的第一道闸门。心得提示词里凡是出现“尽量”“适当”“酌情”这类模糊副词基本都是隐患。能量化的地方全部量化能写死的地方全部写死。模型的自由发挥空间只应留在真正需要创造力的地方。2.2 工具定义函数签名就是一份契约工具是 Agent 唯一能改变外部世界的手段所以它的定义质量直接决定成功率。我的经验是工具描述要写给“一个完全不了解你代码库的人”看因为模型就是那个人。一个反例是参数名为q、描述为“查询”的工具。模型看到这个只能靠猜——猜该传关键词还是完整句子猜要不要加引号。正例是参数名query描述写清“自然语言查询语句包含核心名词即可不需要完整句子长度控制在 20 字以内”。后者能把参数错误率降一个量级。几条硬规矩我基本不破一个工具只做一件事。把“检索并总结”合成一个工具等于把决策权交还给了工具内部模型就失去了调整空间。参数尽量用基础类型。嵌套对象和数组会让模型在生成 JSON 时更容易出错能用字符串和数字解决的就别上复杂结构。返回值要有长度上限。工具返回几万字原文等于直接把上下文撑爆。我一般在工具内部就做截断返回前 800 到 1500 字并标注“已截断如需更多请缩小范围”。错误信息要可读。工具抛异常时不要只返回一个 traceback而是返回一句人能看懂的话比如“文件不存在可用文件列表为a.txt, b.txt”。模型看到这句下一轮往往能自己纠正。这条最后一点特别值得展开。工具报错其实是 Agent 自我修正的最好机会但前提是错误信息以模型能理解的方式回传。我见过不少实现工具一出错就整个流程崩掉白白浪费了模型的纠错能力。2.3 上下文管理别让对话变成一锅粥上下文窗口是 Agent 最稀缺的资源也是最容易被忽视的地方。一个跑了七八轮的任务上下文里会堆积大量中间结果其中很多已经没用了——比如早期一次失败检索返回的空结果。我用的策略是分层保留系统提示词永远保留在最前每一轮的“思考”文本可以压缩甚至丢弃只保留工具调用和结果工具结果按时间做滑动窗口只保留最近 N 条完整内容更早的替换成一句话摘要比如“第 2 轮检索‘成本核算’返回 3 条片段”。这样做的好处是上下文占用可控而且模型看到的都是有效信息。坏处是可能丢掉早期关键线索所以我在摘要里会保留关键词让模型知道“这件事我查过查的是什么”。另一个坑是重复内容累积。如果模型连续三次用几乎相同的参数检索上下文里就会有三份高度相似的结果既浪费 token 又干扰判断。我的做法是在工具层加一个简单的缓存相同参数在同一个任务内只真正执行一次后续直接返回缓存结果并注明“该查询已执行过结果同上”。这一招对成本和延迟的改善非常明显。2.4 输出解析给模型的自由加上护栏让模型输出结构化数据是 Agent 落地到业务系统的必经之路但也是翻车高发区。模型很可能会在 JSON 外面裹一层 json 代码块可能会在前后加一句“好的以下是结果”也可能少个引号。我的处理是三层防御提示词层明确写“只输出 JSON不要任何解释文字不要使用代码块标记”。这能解决八成问题。解析层先用宽松方式剥离可能的代码块标记和前后缀再尝试解析。解析失败不直接抛错。修复层解析失败时把错误信息和原始输出一起回传给模型让它自己修一次。实测这一次修复的成功率相当高通常能到七成以上。如果三层都失败才走兜底逻辑——返回一个带parse_error标记的结构把原始文本塞进去交给人工或下游处理。这里的关键认知是不要指望模型 100% 输出合法 JSON要假设它一定会出错然后设计出错之后的路径。3. 实操搭一个能跑的资料整理 Agent3.1 环境准备与最小依赖我倾向于把依赖压到最少第一个项目只装两样东西一个 HTTP 请求库和一个环境变量管理库。不需要 Web 框架不需要数据库不需要向量库——资料量小的时候直接用关键词匹配检索完全够用先跑通流程再谈优化。python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install requests python-dotenv环境变量里只放两样接口地址和密钥。这里不写具体厂商因为不同平台的接口形态差异不小但对话接口的请求体结构大同小异都是消息数组加若干参数。# .env LLM_BASE_URLhttps://your-endpoint.example.com/v1/chat LLM_API_KEYyour_key_hereimport os from dotenv import load_dotenv load_dotenv() BASE_URL os.getenv(LLM_BASE_URL) API_KEY os.getenv(LLM_API_KEY)注意密钥永远不要写进代码也不要在日志里打印完整请求头。排查问题时打印前六位加长度就够了这个习惯能省掉很多麻烦。3.2 工具层三个函数撑起整个能力边界第一个项目我只定义三个工具够用且不臃肿列文件、检索、读章节。import os, json, glob DOC_DIR ./docs def list_docs(): 列出资料库中所有可用文件名。 files [os.path.basename(p) for p in glob.glob(os.path.join(DOC_DIR, *.txt))] if not files: return 资料库为空。 return 可用文件 , .join(files) def search_docs(query: str, top_k: int 3): 按关键词在资料库中检索返回最相关的若干片段。 if not query or len(query) 50: return 查询语句无效请提供 50 字以内的关键词。 hits [] for path in glob.glob(os.path.join(DOC_DIR, *.txt)): with open(path, encodingutf-8) as f: text f.read() for para in text.split(\n\n): if query.lower() in para.lower(): hits.append({file: os.path.basename(path), text: para[:600]}) if not hits: return f未检索到与「{query}」相关的内容。 return json.dumps(hits[:top_k], ensure_asciiFalse) def read_section(doc_name: str, keyword: str): 读取指定文档中包含关键词的完整段落。 path os.path.join(DOC_DIR, doc_name) if not os.path.exists(path): return f文件 {doc_name} 不存在。{list_docs()} with open(path, encodingutf-8) as f: text f.read() blocks [b for b in text.split(\n\n) if keyword in b] if not blocks: return f在 {doc_name} 中未找到包含「{keyword}」的段落。 result \n\n.join(blocks) if len(result) 1500: result result[:1500] \n[内容已截断请用更具体的关键词缩小范围] return result三个函数都不复杂但每个都埋了前面说的那几条规矩查询长度校验、结果数量上限、文本截断、错误信息可读。特别是read_section里文件不存在时顺手把可用文件列表返回去模型下一轮基本能自己改对文件名这个小细节减少了我很多次手动介入。工具定义还要转成模型能理解的描述格式也就是一份 JSON SchemaTOOLS [ { type: function, function: { name: search_docs, description: 在资料库中按关键词检索相关段落。不确定信息位置时优先调用。, parameters: { type: object, properties: { query: {type: string, description: 检索关键词20 字以内优先用名词}, top_k: {type: integer, description: 返回条数默认 3} }, required: [query] } } }, # list_docs 与 read_section 同理此处略 ]描述文案里的每一个字都算数。“不确定信息位置时优先调用”这句话比写十遍“请谨慎使用工具”都管用。3.3 主循环ReAct 骨架的极简实现核心循环其实就是个 while但写得糙和写得稳差别在细节上。import requests, json, time MAX_TURNS 8 TIMEOUT 30 def call_llm(messages, toolsTOOLS): for attempt in range(3): try: resp requests.post( BASE_URL, headers{Authorization: fBearer {API_KEY}, Content-Type: application/json}, json{messages: messages, tools: tools, temperature: 0.2}, timeoutTIMEOUT ) resp.raise_for_status() return resp.json() except Exception as e: if attempt 2: raise time.sleep(2 ** attempt) # 指数退避1s, 2s def run_agent(question: str): messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: question} ] trace [] for turn in range(MAX_TURNS): result call_llm(messages) msg result[choices][0][message] messages.append(msg) # 没有工具调用说明模型给出了最终答案 if not msg.get(tool_calls): return msg[content], trace for call in msg[tool_calls]: name call[function][name] try: args json.loads(call[function][arguments]) except json.JSONDecodeError: messages.append({ role: tool, tool_call_id: call[id], content: 参数 JSON 解析失败请检查格式后重试。 }) continue fn {list_docs: list_docs, search_docs: search_docs, read_section: read_section}.get(name) output fn(**args) if fn else f未知工具{name} trace.append({turn: turn, tool: name, args: args, output: output[:200]}) messages.append({ role: tool, tool_call_id: call[id], content: output }) # 轮次耗尽强制收尾 messages.append({role: user, content: 已达到最大轮次请基于现有信息直接输出 JSON 结果。}) final call_llm(messages, tools[]) return final[choices][0][message][content], trace这段代码里有几个值得说的设计点。温度设为 0.2。Agent 任务要的是稳定和守规矩不是文采。温度调高会让模型在参数生成上更“活泼”也更容易瞎编参数值。做工具调用低温几乎是标配。指数退避重试。网络抖动和接口限流是常态一次失败就崩掉太脆。1 秒、2 秒的退避简单有效加上三次上限基本能覆盖大部分瞬时故障。参数解析失败也要回传。模型偶尔会生成不合法 JSON这时候如果直接抛异常整个流程就死了。把错误当作一次工具结果塞回上下文模型通常下一轮就能改对。轮次耗尽后的强制收尾。这是很多人会忘的一步。如果不加Agent 跑到最大轮次后会直接返回空或者抛错前面的工作全废。我的做法是追加一条指令并且这次调用不再给工具——断了它继续折腾的念想逼它立刻总结。全程留 trace。每一轮的工具有哪些参数、返回了什么截断到 200 字全部记下来。这份 trace 在排障时的价值比任何日志框架都大。3.4 参数调优几个必须手动算一算的数最大轮次 MAX_TURNS怎么定我的算法是典型任务需要的最少工具调用次数乘以 2再加 2。资料整理任务一般两到四次工具调用能收敛所以 8 是比较舒服的上限——既给了纠错空间又不会让跑偏的任务无限烧钱。单次请求超时设 30 秒。这个数字跟模型的输出长度有关长文本生成会更久但 Agent 场景里每轮输出通常很短就是一次工具调用30 秒足够。超时设太长的代价是故障时用户干等体验更差。工具返回长度控制在 600 到 1500 字之间。这个量级能覆盖一个完整段落的信息量又不至于几轮就把上下文塞满。如果你的任务是长文档分析就得配合前面说的滑动窗口策略。成本粗算。假设系统提示词 800 token每轮工具结果 500 token模型每轮输出 200 token跑 5 轮输入大致累加第 1 轮 1000第 2 轮 1700第 3 轮 2400第 4 轮 3100第 5 轮 3800合计约 12000 token输出合计约 1000 token注意这个累加特性——上下文的成本是平方级增长的因为每一轮都要把历史全带上。这就是为什么轮次上限和结果截断不是可选项而是成本控制的核心手段。一个失控的 20 轮任务成本可能是正常任务的十倍以上。4. 排障实录那些只有跑起来才会遇到的问题4.1 死循环与工具调用风暴最常见的故障是 Agent 反复调用同一个工具参数几乎不变五六轮过去还在原地。触发原因通常有两类一是检索确实没找到答案模型不甘心二是它在两个工具之间来回横跳A 说信息不足去查 BB 说去查 A。我的处理分三层。提示词层设定明确的次数上限并声明“超过后必须作答”这是第一道闸门。代码层做一个简单的重复检测如果连续两轮的工具名和参数完全一致直接在返回内容里加一句“该查询已执行过结果与上次相同请换一个思路或直接作答”。最后一层是 MAX_TURNS 兜底。实测下来第二层效果最好。因为模型看到“你已经查过这个了”这句话往往会意识到自己陷入了循环转而尝试别的路径。4.2 参数幻觉模型编造了不存在的值典型场景文件明明叫report_2023.txt模型传了report.txt日期参数要求YYYY-MM-DD它传了“上个月”。这类问题的根因是模型手里没有准确信息只能靠猜。两个应对办法配合使用效果最好。第一在系统提示词里明确“调用工具前若不确定文件名先调用 list_docs 获取列表”。第二也是更重要的工具的错误返回值要带上正确的候选值——就像前面read_section里做的那样把可用文件列表直接塞回去。模型看到列表下一轮基本能挑对。如果某个参数取值空间很小比如只有三种状态直接把它写成枚举塞进工具描述里比让模型自由生成靠谱得多。4.3 上下文被撑爆的三种表现上下文超限不一定报错它有几种很隐蔽的表现形式。第一种是中间部分的信息被“遗忘”——模型明明前面查到了答案后面却说没找到。第二种是输出质量突然下降开始胡言乱语。第三种才是接口直接返回长度超限错误。排查方法很朴素把每次请求的 messages 打印出来数一下字符数。如果发现某一轮突然从 3000 字跳到 15000 字基本就是某个工具返回了超长内容。对策按优先级排先缩短工具返回值这是治本再精简系统提示词删掉那些“以防万一”的冗余段落最后才考虑滑动窗口和摘要压缩。我见过不少人一上来就搞复杂的上下文压缩算法其实把工具返回截断一下问题就没了。4.4 常见问题速查表现象大概率原因处理动作反复调用同一工具信息不足或不甘心重复检测 提示词轮次上限输出不是合法 JSON提示词未约束或温度过高强化格式约束 降温度 自修复重试中途卡住无响应网络超时或接口限流超时设置 指数退避重试回答张冠李戴工具返回内容混淆在结果中标注来源文件名成本异常高轮次过多或上下文膨胀检查 MAX_TURNS 与返回值长度强制收尾时输出为空未做兜底指令追加“直接输出结果”并禁用工具4.5 一个容易被忽略的坑工具描述与实现不一致我踩过一次很隐蔽的坑工具描述里写“返回最相关的前三条”实现里因为改代码忘了同步变成了返回全部。结果上下文被瞬间撑大模型开始丢信息排查了半天才发现是文档和实现脱节。从那以后我养成一个习惯工具的描述文案和实现放在同一个文件里改实现必须看到描述。同时给每个工具写一个最小单元测试测返回长度是否超限、错误路径是否返回了可读信息。这几个测试写起来十分钟能省掉几小时的迷茫。5. 跑通之后才开始的功课5.1 评估集先建一个能重复跑的对照组第一个版本跑通后很容易陷入“改一版提示词手动试一下感觉好像好点了”的循环。这种凭感觉调优效率极低因为你没法判断改动到底是真提升还是随机波动。我的做法是攒一个二十条左右的评估集覆盖正常路径和各类边界正常问题、资料里没有答案的问题、需要多跳检索的问题、文件名容易混淆的问题、超长文档的问题。每条写清楚预期行为比如“应当返回 confidence 为 0”或者“必须引用具体文件”。每次改提示词或换参数跑一遍这二十条人工看一眼通过率。这个流程土但极其有效。我印象最深的一次是某个“看起来更清晰”的提示词改版评估集一跑边界场景通过率从 16/20 掉到了 11/20及时拦住了。5.2 日志字段留下能复现的现场日志不要只记“成功/失败”那样出了问题根本没法复现。我至少要记这几个字段任务 ID、轮次、工具名、完整参数、返回内容长度不是内容本身避免日志爆炸、本次调用的输入 token 和输出 token、耗时。有了这些任何一次异常都能还原出完整执行路径。特别是参数和返回长度这两个前者定位幻觉后者定位上下文膨胀几乎覆盖了七成以上的问题。心得日志里一定要有任务 ID 并贯穿始终。单次调试时觉得多余一旦并发跑起来或者做批量评估没有它就完全没法把散落的记录拼回一次会话。5.3 扩展方向从能跑到好用这套骨架跑顺之后扩展路径其实很清晰我大致按投入产出比排了个序。第一优先级是换掉朴素检索。现在是关键词完全匹配稍微换个说法就查不到。可以先用更宽松的匹配策略比如分词后按命中词数排序成本几乎为零效果提升却很直接。第二优先级是加一层结果缓存。同样的工具调用参数不要重复执行尤其在调试阶段反复跑同一批问题时缓存能省掉大量时间和调用成本。第三优先级才是换更好的模型或者上向量检索。这两件事都需要额外投入等你的评估集显示瓶颈确实在这里再做别一上来就堆料。至于多 Agent 协作我的看法是当你发现单 Agent 的提示词已经长到你自己都维护不动或者它需要同时具备两种互相冲突的行为模式时才考虑拆分。拆分之前一定要确保单 Agent 版本的评估集跑得足够稳否则你只是把一个可控问题变成了三个不可控问题。我自己在这几个小项目里最深的体会是Agent 的工程质量八成取决于你喂给模型的那段上下文有多干净、工具的错误信息有多友好、循环的边界设得有多明确。模型能力当然重要但它更像是给这套框架加的一个乘数而不是让它跑起来的那个基底。把基底打牢换哪个模型都能跑得不错基底是虚的模型再强也救不回来。

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

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

免费获取报价 →
↑