ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

从零手写Agent最小系统:LLM工具调用与上下文管理实战

2026/10/3 5:39:16 拓冰建站 浏览量
从零手写Agent最小系统:LLM工具调用与上下文管理实战 1. 从热词里拆出真实需求Agent到底在解决什么问题先把结论摆在前面Agent这个词被炒得很热但剥开外壳它要解决的核心问题只有一个——让大模型从一问一答变成能自己跑完一件事。普通对话式LLM你问一句它答一句任务边界完全由你划而Agent的本质是给它一个目标让它自己决定下一步做什么、调用什么工具、什么时候停下来。我接触Agent开发这两年最大的感受是很多人一上来就冲着框架去LangGraph、Swarm、各种编排平台挨个试一遍结果连最基础的循环机制都没搞明白。热词里那几个词其实已经把Agent的骨架暴露得很清楚了——Agent、LLM、工具调用、上下文、循环机制这五个词就是一套完整的Agent最小系统。LLM是大脑负责推理和决策工具调用是手脚负责和外部世界交互上下文是记忆决定它每一步能看到什么信息循环机制是心跳驱动它一轮一轮往前走。缺任何一个这东西都跑不起来。你去看那些热词agent框架与编排agent架构agent安全ai agent怎么扛并发全是围绕这五个点衍生出来的工程问题。这篇文章我打算按我自己搭Agent的顺序来拆先讲整体设计思路和方案选型再逐个拆核心模块的实现细节然后是完整的实操流程最后把我踩过的坑和排查经验整理出来。适合两类人看——一类是刚搞懂LLM API调用、想往Agent方向走的开发者另一类是已经在用现成框架、但总觉得黑盒太多、控制不住的工程师。我不打算只讲概念每个环节都会给到能直接抄的参数和代码结构。2. 内容整体设计与思路拆解2.1 为什么我不建议一上来就用重框架市面上的Agent框架大致分三档。最轻的是自己写循环几十行代码搞定中间档是LangGraph这类状态机式的编排框架最重的是各种Agent平台拖拽配置就能跑。我的建议很直接第一个Agent一定要手写循环。原因不复杂——框架帮你封装了循环、上下文管理、工具调度但一旦出问题你根本不知道是模型的问题、上下文的问题还是框架的问题。热词里有人问codex无法发送消息显示更新agent沙盒这种问题在自写循环里几乎不会遇到因为每一步你都能打日志。手写一遍之后你会彻底理解所谓Agent就是一个while循环每轮把历史消息喂给LLMLLM返回要么是最终答案、要么是一个工具调用请求如果是工具调用就执行、把结果塞回消息列表然后进入下一轮。就这么简单。理解了这层再去看LangGraph的节点和边你会发现它无非是把循环拆成了显式的图结构方便你做分支和持久化。2.2 方案选型的三个判断维度选框架还是手写我一般看三个维度。任务复杂度。如果任务就是查个天气再总结一下手写循环足够。如果任务有明确的多阶段、需要人工介入、需要断点续跑那LangGraph这类状态机的价值就出来了它把状态显式化了你可以随时保存和恢复。可控性要求。金融、医疗这类场景每一步决策都要可审计那必须手写或者用足够透明的框架。热词里那个LLM驱动的公立医院债务风险智能预警就是典型——这种场景你敢让框架黑盒决策吗不敢。迭代速度。如果只是快速验证一个想法用平台拖拽最快。但一旦要上生产平台往往成为瓶颈因为你想改的地方它不让你改。我自己的习惯是原型阶段手写验证可行后再决定要不要引入框架。很多时候验证完发现手写的版本已经够用了根本不需要框架。2.3 上下文才是真正的战场热词里关于上下文的词特别多上下文影响执行上下文1m上下文是什么意思大模型上下文窗口用完了怎么办上下文长度上下文学习示例选择策略。这不是偶然Agent的成败八成取决于上下文管理。为什么因为LLM是无状态的它每一轮看到的只有你塞给它的消息列表。这个列表里放什么、放多少、怎么排序直接决定了它的决策质量。放太少它不知道之前干了什么放太多超出窗口或者被无关信息干扰。我见过太多Agent跑着跑着就失忆或者发疯追根溯源都是上下文管理没做好。所以下面我会花很大篇幅讲这块包括窗口快满了怎么办、历史怎么压缩、工具返回结果怎么裁剪。3. 核心细节解析与实操要点3.1 LLM作为决策核心怎么让它稳定输出结构化结果Agent的第一块基石是让LLM稳定地输出我要调用哪个工具、参数是什么。这里最大的坑是模型输出格式不稳定。你让它返回JSON它有时候给你包一层markdown代码块有时候加一句好的我来帮你调用。解决办法有两个层次。第一层是用模型原生的工具调用能力function calling / tool use主流模型都支持你传一个工具schema数组模型会返回结构化的调用请求。这是最稳的能用就用。第二层是如果模型不支持或者你要兼容多个模型那就用提示词约束加解析兜底——明确要求只输出JSON不要任何其他文字然后在代码里做容错解析比如用正则提取第一个{...}块。我实测下来工具schema的描述质量比模型本身更影响调用准确率。工具名要语义清晰参数描述要写清楚每个字段的含义和格式。比如一个查订单的工具参数order_id的描述写成订单编号格式为纯数字字符串例如12345678比只写订单号的准确率高一大截。提示工具数量超过10个之后模型选错工具的概率明显上升。这时候要么做工具分组、按场景动态注入要么在提示词里给出什么情况用什么工具的明确指引。3.2 工具调用从定义到执行的完整链路工具调用这条链路拆开是四步定义schema、模型决策、执行、结果回填。定义schema时我习惯把工具分成三类查询类只读无副作用、操作类有副作用比如发消息、下单、计算类纯本地计算。分类的意义在于——操作类工具一定要加确认机制不能让Agent自己就把钱花了。执行环节有个容易被忽略的点超时和异常处理。工具调用可能失败、可能超时、可能返回一堆垃圾。如果不处理整个循环就卡死了。我的做法是每个工具调用都包一层超时设个上限比如10秒失败就返回一个结构化的错误信息给模型让它自己决定重试还是换方案。结果回填也有讲究。工具返回的原始数据往往很长比如一个API返回了几KB的JSON直接塞进上下文会迅速撑爆窗口。所以要做结果裁剪——只提取模型决策需要的字段。比如查天气的API返回了20个字段你只需要温度和天气状况那就只回填这两个。# 工具执行与结果裁剪的简化结构 def execute_tool(tool_name, tool_args): try: raw TOOL_REGISTRY[tool_name](**tool_args) # 裁剪只保留关键字段 trimmed trim_result(tool_name, raw) return {status: success, data: trimmed} except TimeoutError: return {status: error, message: 工具执行超时请重试或换方案} except Exception as e: return {status: error, message: f工具执行失败{str(e)}}3.3 上下文管理窗口快满了到底怎么办这是我最想展开讲的部分。热词里大模型上下文窗口用完了怎么办被反复问说明这是真痛点。先明确一个概念上下文窗口是硬上限不是软建议。比如模型窗口是128K token你塞进去130K要么报错要么被截断。而且就算没超塞太满也会导致模型注意力涣散中间的信息容易被忽略。我的处理策略是分层第一层控制单轮注入量。工具返回结果严格裁剪历史消息只保留必要的。系统提示词尽量精简别写一大段废话。第二层历史压缩。当消息列表超过某个阈值比如占窗口的60%就触发压缩。压缩不是简单截断而是让LLM把前面的对话总结成一段摘要用摘要替换掉原始消息。这样既保留了关键信息又大幅缩短了长度。第三层外部记忆。真正重要的信息比如用户偏好、任务关键参数不放在对话历史里而是存到外部数据库或文件需要时再检索注入。这就是RAG思路在Agent里的应用。注意压缩历史时一定要保留最近几轮的完整消息因为模型对最近上下文最敏感。只压缩中间和早期的部分。3.4 循环机制什么时候停比什么时候跑更重要循环机制的核心问题不是怎么循环而是怎么停。我见过太多Agent陷入死循环——反复调用同一个工具、反复输出同样的内容、或者在一个无解的问题上打转。我的做法是设三重刹车最大轮数限制。比如最多15轮到了就强制停止并返回当前状态。这是兜底。重复检测。如果连续两轮调用了同一个工具、参数也几乎一样就判定为卡住中断并提示模型换思路。无进展检测。如果连续几轮没有产生新的有效信息比如工具一直返回错误也中断。停止条件的设计直接关系到成本和体验。轮数设太少任务没跑完就停了设太多万一卡住就是烧钱。我一般从10轮起步根据任务复杂度调整。4. 实操过程与核心环节实现4.1 环境准备与最小依赖手写一个Agent依赖其实很少。核心就两样一个能调LLM的SDK一个HTTP库如果工具有网络请求。我用Python举例需要的东西pip install openai requests如果你用的是其他模型换成对应的SDK即可。不需要LangChain、不需要任何框架先把最小系统跑通。4.2 第一步定义工具集先定义两三个简单工具别一上来就搞复杂的。我用查时间和算数学两个工具做演示一个无副作用、一个有明确输入输出。import datetime import json def get_current_time(timezoneUTC): return {time: datetime.datetime.utcnow().isoformat(), timezone: timezone} def calculate(expression): # 生产环境千万别用eval这里仅为演示 try: result eval(expression, {__builtins__: {}}, {}) return {result: result} except Exception as e: return {error: str(e)} TOOLS { get_current_time: get_current_time, calculate: calculate, } # 给模型看的schema TOOL_SCHEMAS [ { type: function, function: { name: get_current_time, description: 获取当前时间当用户询问现在几点、今天日期时使用, parameters: { type: object, properties: { timezone: {type: string, description: 时区默认UTC} }, }, }, }, { type: function, function: { name: calculate, description: 执行数学计算当需要做算术运算时使用, parameters: { type: object, properties: { expression: {type: string, description: 数学表达式如 23*4} }, required: [expression], }, }, }, ]注意description我写得比较具体明确说了什么时候用。这是提升调用准确率的关键别偷懒。4.3 第二步写主循环主循环是整个Agent的心脏。逻辑就是前面说的喂消息、拿决策、执行工具、回填结果、判断是否结束。from openai import OpenAI client OpenAI() def run_agent(user_input, max_turns10): messages [ {role: system, content: 你是一个助手可以使用工具完成任务。任务完成后直接给出答案。}, {role: user, content: user_input}, ] for turn in range(max_turns): response client.chat.completions.create( modelgpt-4o-mini, messagesmessages, toolsTOOL_SCHEMAS, tool_choiceauto, ) msg response.choices[0].message # 没有工具调用说明任务结束 if not msg.tool_calls: return msg.content # 把模型的决策加入历史 messages.append(msg) # 执行每个工具调用 for tool_call in msg.tool_calls: name tool_call.function.name args json.loads(tool_call.function.arguments) result execute_tool(name, args) messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse), }) return 达到最大轮数限制任务未完成这段代码不到40行但已经是一个能跑的Agent了。你可以拿现在几点了然后帮我算一下从今天到年底还有多少天这种需要多步的任务去测看它怎么一步步调用工具。4.4 第三步加上上下文压缩上面的版本跑长任务会撑爆窗口。加一个压缩函数在每轮开始前检查消息长度。def estimate_tokens(messages): # 粗略估算中文约1.5字符/token英文约4字符/token total 0 for m in messages: content m.get(content) or total len(str(content)) / 2 return total def compress_history(messages, threshold6000): if estimate_tokens(messages) threshold: return messages # 保留系统消息和最近4条中间的做摘要 system_msg messages[0] recent messages[-4:] middle messages[1:-4] if not middle: return messages summary_prompt 请用简洁的中文总结以下对话的关键信息和已完成的操作\n summary_prompt \n.join([str(m) for m in middle]) summary_resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: summary_prompt}], ) summary summary_resp.choices[0].message.content return [system_msg, {role: system, content: f历史摘要{summary}}] recent在主循环开头调用messages compress_history(messages)即可。阈值根据你模型的窗口大小调整一般取窗口的50%到60%比较安全。4.5 第四步加刹车机制最后加上防死循环的逻辑。记录每轮的工具调用检测重复。def run_agent_with_guard(user_input, max_turns10): messages [...] call_history [] for turn in range(max_turns): messages compress_history(messages) response client.chat.completions.create(...) msg response.choices[0].message if not msg.tool_calls: return msg.content # 检测重复调用 current_calls [(tc.function.name, tc.function.arguments) for tc in msg.tool_calls] if current_calls in call_history[-2:]: return 检测到重复操作任务可能陷入循环已中断 call_history.append(current_calls) # ... 执行工具、回填结果到这里一个带上下文管理和防死循环的Agent就成型了。总共一百多行但核心机制全都有了。5. 常见问题与排查技巧实录5.1 模型不调用工具直接瞎编答案这是最常见的。模型明明有工具可用却直接凭记忆回答。原因通常是工具描述不够明确或者系统提示词没强调必须用工具获取实时信息。我的排查顺序先看工具schema的description是不是太模糊再看系统提示词有没有明确要求最后看tool_choice参数实在不行设成required强制它必须调用工具但这样它就没法直接回答了要慎用。5.2 工具调用参数格式错误模型返回的参数JSON解析失败或者字段类型不对。这个多半是schema里参数描述不够清楚。比如你要求一个整数但没说明模型可能传字符串。解决办法是在参数description里写清楚类型和示例然后在代码里做类型转换兜底。别指望模型100%听话。5.3 上下文超限报错报错信息一般是context length exceeded之类。这时候要么触发压缩要么裁剪工具返回结果。我建议两个都做——压缩是长期策略裁剪是每轮都要做的。5.4 Agent跑着跑着开始重复典型的循环卡死。检查你的重复检测逻辑有没有生效以及工具是不是一直返回同样的错误导致模型反复重试。如果是工具本身的问题先修工具。5.5 并发场景下的状态污染热词里有人问ai agent怎么扛并发。核心问题是如果你的Agent用全局变量存状态多个请求同时进来就会串。解决办法是每个请求一个独立的messages列表和状态对象绝不共享可变状态。如果要用外部存储用请求ID做隔离。问题现象最可能原因排查动作不调用工具直接回答工具描述模糊/提示词没约束检查schema和system prompt参数解析失败参数类型描述不清补充description加类型转换上下文超限历史或工具结果太长开启压缩裁剪工具返回反复调用同一工具无重复检测/工具持续报错加重复检测修工具并发下结果错乱共享了可变状态每请求独立状态对象5.6 几个我踩过的坑第一个坑是过度依赖模型的自觉。早期我总觉得提示词写清楚就行结果模型该犯错还是犯。后来明白能用代码约束的绝不用提示词——格式用schema约束流程用代码控制提示词只负责语义理解。第二个坑是工具返回结果不裁剪。有次一个搜索工具返回了整页HTML直接把上下文撑爆Agent当场失忆。从那以后我所有工具返回都做字段白名单。第三个坑是没设最大轮数。有次测试一个任务Agent卡在循环里跑了上百轮账单直接起飞。现在我的默认值就是10轮特殊任务才调高。6. 从最小系统到生产级Agent的扩展方向把上面这套跑通之后你会发现扩展方向很清晰。加持久化。把messages存到数据库支持断点续跑。这就是LangGraph那类框架帮你做的事但你手写也不难。加多Agent协作。热词里提到Swarm框架的agent、handoff与上下文变量本质就是多个Agent之间传递控制权和上下文。你可以先从一个主Agent加几个专职子Agent开始主Agent负责调度子Agent负责具体领域。加评估。热词里有llm as judge就是用另一个LLM来评判Agent的输出质量。这在没有标准答案的任务里特别有用可以自动化跑回归测试。加安全边界。操作类工具一定要有权限控制和确认机制。热词里agent安全不是空话一个能自主调用工具的Agent如果工具权限过大风险是实打实的。我自己在实际操作中的体会是Agent这东西框架能帮你省事但省不掉理解。你把最小循环、上下文管理、工具调用这三件事亲手实现一遍再去看任何框架都会觉得通透。反过来如果跳过这步直接上框架遇到问题就只能干瞪眼。所以如果你刚开始搞Agent别急着追新框架先把那一百多行手写循环跑明白后面的一切都是在这上面加东西。