
先放个观点模型不是 Agent。这句话我最近在好几个项目里反复体会到尤其是当你试图让一个大语言模型“自己完成一件需要多步操作的事”时模型本身的局限性会非常明显。模型能做的是根据输入生成输出但 Agent 要做的是一件事从目标出发通过感知环境、调用工具、观察结果、调整策略最终把目标完成。这个循环就是 Agent Loop也是我从零实现一个最小可用 Agent 时最先搞清楚的东西。这篇文章会从“模型和 Agent 到底差在哪”讲起然后拆解一个最小 Agent Loop 的四个核心环节再给出一套可以直接跑起来的 Python 代码框架最后聊聊我在实操中踩过的坑和排查思路。不管你是想入门 Agent 开发还是已经写过一些 Demo 但总觉得不“Agent”这篇文章应该都能给你一些可落地的参考。内容不依赖任何重型框架底层逻辑讲透之后你去看 LangChain、AutoGPT、MetaGPT 之类的项目也会更容易理解它们到底在做什么。1. 模型和 Agent 之间差的不是一个概念而是一个 Loop1.1 模型是“脑子”Agent 是“手脚加脑子”的循环经常有人把“调一下大模型接口”叫成“做了一个 Agent”但严格来说这只是在调用一个模型。模型本质上是函数输入文本输出文本。你把一段 prompt 丢进去它基于训练时学到的概率分布吐出一段回答这个过程没有目标没有环境没有反馈更没有连续行动。Agent 不一样Agent 是围绕一个目标持续运行的系统它有感知、有决策、有动作还会根据动作的结果修正下一步动作。怎么理解打个比方模型像一个很聪明但不会动手的顾问。你问他“北京今天适合出门吗”他能根据训练数据里的常识给你一通分析。但如果你让他“帮我看看北京现在下雨没然后再查一下未来三小时有没有雨最后告诉我该不该带伞”单次调用模型他只能给你一个“建议”而不是真正去查数据。Agent Loop 做的事情就是给这个聪明顾问配上查询天气的“手”让他自己去查、去比对、去得出结论并且这个过程中他可以多次查询、多次思考直到把事情办完。所以我说模型不是 Agent这句话真正的含义是模型只是 Agent 内部那个负责“思考”的引擎Agent 本身是一个包含思考、行动、观察、再思考的循环结构。这也是 Agent Loop 这个名字的来源英文里的 loop 就是循环。1.2 为什么光靠模型调用干不了“多步实事”单次调用模型干不了多步实事原因其实不复杂。第一模型没有自主性你问一句他答一句你不继续问他就不继续动。第二模型没有工具他不能真的去查数据库、调接口、操作文件只能基于训练数据做推断。第三模型没有反馈机制他生成完一段文字就结束了不会知道这个结果到底有没有用、地方对不对、下一步该不该调整。举个例子你想让模型“把当前目录下所有大于 10MB 的文件列出来并按大小排序”。如果只是调用模型他能给你一段 Python 代码但不会替你执行更不会帮你把执行结果拿回来继续处理。Agent Loop 就能解决这个问题模型先生成一段代码Agent 把代码放到终端里跑然后把输出回传给模型模型基于输出继续整理成你要的列表。这个“生成工具调用、执行工具、回填结果、继续推理”的循环就是 Agent 的骨架。我在最初做 Agent 的时候最大的误区就是把 prompt 写得特别复杂妄想让模型“一步到位”。后来我发现真正让 Agent 变实用的不是把 prompt 写得天花乱坠而是把 Loop 搭好让模型在循环里自然地进行多步推理。接下来我就拆一下这个最小 Loop 到底由哪些部分组成。2. 最小 Agent Loop 的四个核心环节2.1 Loop 的本质思考、行动、观察、再思考一个最小可用的 Agent Loop抽象出来就四个环节思考Think、行动Act、观察Observe、再思考Repeat。模型在循环里不是只生成一次而是生成多次每次生成的内容会被 Agent 解析如果发现模型想调用某个工具Agent 就执行这个工具把结果作为新的上下文喂给模型模型再继续生成。直到某次生成里模型不再调用工具而是给出了最终答案循环才结束。这跟人类的做事方式很像。你打算出去玩第一步是看天第二步是决定要不要带伞第三步是出门前再看一眼窗外确认没有变天第四步再决定最终带不带。Agent 也一样每一步“看”到的信息都会影响下一步“想”的方向。这种循环结构最大的优点是让整个系统的行为变得可以拆解你可以随时介入、随时调试而不是靠一个大 prompt 相信模型能一次搞定。我再强调一遍这四步缺一不可。没有“思考”模型输出就没有方向没有“行动”模型只能纸上谈兵没有“观察”模型不知道自己刚才做的有没有用没有“再思考”整个流程就断了没法逼近最终目标。2.2 工具调用的格式设计让模型“会用人手”要让模型在循环里调用工具首先得让模型“知道”有哪些工具可用以及怎么调用。目前主流做法是在请求里带上工具定义让模型生成一个结构化的“想要调用某个函数”的指令而不是让模型自己在文本里写“我想调用查天气函数”。以 OpenAI 的 function calling 格式为例工具定义是一个 JSON Schema里面包含工具的名称、描述、参数和参数类型。模型看到这些定义之后如果觉得某个工具对当前任务有帮助就会在返回内容里带一个tool_calls字段里面写明要调用哪个函数、参数是什么。Agent 拿到这个字段之后自己去执行对应的函数再把结果以tool消息的形式返回给模型。这个设计很好理解模型不是真的能执行工具而是“提出一个调用工具的请求”真正执行的还是你的代码。所以工具格式设计得好不好直接影响模型能不能准确调用。我的经验是工具描述一定要写清楚“什么时候用”和“怎么用”比如“当用户询问天气时调用此工具参数 city 是城市中文名”模型理解起来会容易很多。如果描述太模糊模型很容易在不需要调用工具的时候乱调用或者在需要调用的时候漏调用。2.3 停止条件与轮数限制没有出口的循环是灾难点Any循环都要考虑出口。Agent Loop 也不例外。如果模型迟迟不给最终答案或者反复调用同一个工具你的程序就会一直跑下去既消耗 token 又浪费时间。所以最小实现里必须设置两样东西最大轮数和停止条件。最大轮数就是 Agent 最多进行多少轮“思考和行动”比如设成 5 或者 10。一旦超过这个数还没出最终答案就强制终止返回一个超时错误。停止条件则是“模型不再请求调用工具而是输出最终答案”这是最自然的循环出口。实操中我还会额外加一个“重复检测”如果模型连续两三次生成完全一样的工具调用大概率是陷入死循环了这时候直接中断比继续跑下去更有意义。这种启发式规则不难实现我就见过很多人在日志里看到一大堆重复的 tool_call其实这就是代码里少了重复检测导致的。2.4 为什么最小实现不需要框架现在市面上有大量 Agent 框架LangChain、AutoGPT、MetaGPT、Microsoft Agent Framework 之类功能看着很全。但我在从零实现最小 Agent Loop 的过程中最大的体会是理解本质之前不要急着套框架。框架帮你封装了太多细节表面上你写两行代码就能跑起来一个 Agent但遇到问题的时候你根本不知道问题出在哪儿是在模型那边工具那边还是编排逻辑那边。自己实现一个最小 Loop 并不难代码量可能就一百行左右。你自己写一遍循环就会很清楚每一步发生了什么模型在什么时候决定调用工具、工具结果怎么回填、上下文怎么组织、什么时候该停止。这些理解会帮助你看框架源码时一眼就知道它们在做什么排查问题也有方向。所以这篇文章接下来我直接给你一个可以运行的最小实现不需要装任何框架只需要有一个支持工具调用的模型接口就行。3. 从零实现一个可直接运行的最小 Agent3.1 技术选型Python 加一个兼容工具调用的模型实现语言我选了 Python原因不用多说生态最简单处理 JSON、HTTP 都很方便。模型方面你只需要一个支持工具调用的模型接口现在很多模型都兼容 OpenAI 的 tools 参数格式比如本地 Ollama 的某些模型、Qwen、DeepSeek 等。为了演示方便我建议你直接用 OpenAI 兼容的接口代码里只需要用requests或者openai的 Python SDK 即可。如果你没有 API Key也可以用本地部署的方案比如 Ollama 里跑一个 qwen2.5 或者 llama3.1 之类的模型本地接口开到http://localhost:11434/v1然后代码里把base_url指过去就行。本地模型的好处是数据不出门、免费缺点是推理速度慢但跑通最小 Loop 完全够用。这里我不限定具体厂商你用自己手头能调通的模型就行。3.2 定义工具先用计算器和“假天气接口”练手定义一个工具本质上就是写一个普通的 Python 函数然后给这个函数配一份 JSON Schema 说明。为了方便演示我定义两个工具一个是calculator用来算加减乘除另一个是get_weather我故意不接真实天气 API而是返回一个固定结果这样你跑起来不需要申请任何第三方 API。为什么要用假天气因为最小 Demo 的重点是“循环怎么跑”不是“工具怎么实现”。等你理解了 Loop 结构再把假工具换成真实 API、数据库查询、文件操作都很容易。工具函数本身和 Agent 循环是解耦的。工具定义示例def calculator(expression: str) - str: # 生产环境不要用 eval这里仅作演示 return str(eval(expression)) def get_weather(city: str) - str: return f{city} 现在小雨未来三小时有小到中雨温度 18-20 度对应工具 Schematools [ { type: function, function: { name: calculator, description: 计算数学表达式的值比如 1 2 * 3, parameters: { type: object, properties: { expression: {type: string} }, required: [expression] } } }, { type: function, function: { name: get_weather, description: 查询指定城市的实时天气和未来三小时预报, parameters: { type: object, properties: { city: {type: string} }, required: [city] } } } ]这里有个细节参数名和函数参数名要保持一致。calculator里参数叫expressionSchema 里 properties 也要叫expression。如果对不上模型生成的参数传到函数里就会TypeError。3.3 核心循环代码一百行以内跑通 Agent Loop下面是我自己常写的最小循环版本你可以直接抄走跑。核心逻辑就一个for循环循环里做三件事调用模型、检查是否有工具调用、如果有就执行工具并回传结果如果没有就输出最终答案结束。import json import requests def chat_once(api_url, api_key, messages, tools): payload { model: 你的模型名, messages: messages, tools: tools, tool_choice: auto } headers {Authorization: fBearer {api_key}} resp requests.post(api_url, jsonpayload, headersheaders) resp.raise_for_status() return resp.json()[choices][0][message] def run_agent(task, api_url, api_key, tools, tool_map, max_steps5): messages [{role: user, content: task}] for step in range(max_steps): print(f\n Step {step 1} ) msg chat_once(api_url, api_key, messages, tools) messages.append(msg) if msg.get(tool_calls): for call in msg[tool_calls]: fn call[function] name fn[name] args json.loads(fn[arguments] or {}) print(f调用工具: {name}, 参数: {args}) result tool_map[name](**args) messages.append({ role: tool, tool_call_id: call[id], content: result }) else: print(最终答案:, msg[content]) return msg[content] raise RuntimeError(超过最大轮数未得到最终答案) if __name__ __main__: api_url http://localhost:11434/v1/chat/completions api_key YOUR_API_KEY tool_map {calculator: calculator, get_weather: get_weather} result run_agent( task帮我计算 (12 34) * 5 的结果然后查一下北京的天气并告诉我出门要不要带伞。, api_urlapi_url, api_keyapi_key, toolstools, tool_maptool_map, max_steps5 ) print(\nAgent 最终返回:, result)这段代码就是最小 Agent Loop 的骨架你把它跑起来之后会看到日志里模型先生成一次决定调用计算器工具工具返回结果模型再生成一次决定调用天气工具天气返回结果最后模型综合结果给出最终答案。整个过程非常直观。几个需要注意的细节第一messages是不断累积的模型每次生成前都能看到前面所有的历史所以工具结果会被自动当作上下文的一部分。第二tool_call_id必须精确回填这是把“工具调用请求”和“工具调用结果”配对的关键字段如果漏掉这个字段很多模型会报错。第三最大轮数不要设太大5 到 10 足够大多数简单任务设太大反而会在死循环时浪费更多时间。3.4 跑起来看日志理解每一步的状态代码写完之后我强烈建议你先跑一个简单任务把日志完整看一遍。你会发现模型第一次生成的tool_calls内容是什么、工具返回之后模型的态度发生了什么变化这些“现场感”是看文档得不到的。我实际跑过一个天气任务日志大概是这样的第一次模型调用get_weather参数是{city: 北京}工具返回“北京小雨”然后模型下一步没有继续调用工具而是直接输出“北京现在下雨出门建议带伞”。整个循环就两步一步行动一步收尾。非常符合直觉。如果你跑出来的结果和预期不一致比如模型一轮就输出最终答案没有调用工具那也不用慌多半是工具描述不清晰或者tool_choice设成了auto之后模型觉得不必调用。你可以把tool_choice强制设为{type: function, function: {name: get_weather}}来逼它必须先查天气这在调试的时候很好用。3.5 从手工循环到循环内的“异常安全”真实场景中工具调用不一定总是成功。比如网络超时、参数传错、模型生成的 JSON 格式不合法。所以最小实现里我还会在工具调用外面套一层 try-except。如果工具抛异常我会把错误信息当作工具结果回传给模型让模型自己决定是换个参数重试还是告诉用户工具出错了。这比让 Agent 直接崩溃要好得多。举个例子如果用户问“计算 10 除以 0”calculator里的eval会抛ZeroDivisionError如果我把异常信息回传给模型模型可能会说“这个表达式无法计算因为除数为零”而不是整个程序崩掉。这个设计叫“错误即观察”在 Agent Loop 里非常实用。很多时候模型比你想的聪明你给它一个错误信息它能自己修正调用方式。4. 实际运行时常见的坑和排查方法4.1 模型就是不调用工具光给你一段话这是我见过最多的问题。现象是任务明明需要查天气模型却直接输出“北京今天可能有雨建议带伞”完全不经过工具。排查思路有三个方向。第一检查工具定义里的描述是否清晰。如果get_weather的描述写的是“一个函数”模型很可能不知道什么时候该用它。改成“当用户查询某个城市的天气或需要基于天气做决策时必须调用此工具”效果会立刻不一样。第二检查模型本身是否支持工具调用。有些模型 API 虽然兼容 OpenAI 格式但工具调用能力很弱可能直接忽略 tools 参数。你可以先手动构造一个简单请求看看返回里有没有tool_calls字段。第三检查tool_choice。如果你设了tool_choice: none模型永远不会调工具这个参数默认是auto但有些 SDK 的默认行为不同显式设置比较稳妥。我自己的经验是前两个原因占了 80%。很多新手一上来就怪模型笨其实就是工具描述没写好。4.2 循环卡死或无限转圈最后报 execution terminated如果你看到日志里模型不断生成tool_calls但始终没有最终答案这就是死循环。最常见的原因是工具执行结果没有改变模型所处的状态比如某个工具不管传什么参数都返回同一句话模型反复尝试也没法推进任务。解决办法有几个。一是设置最大轮数这是底线保证程序不会无限跑。二是加重复检测比如相邻两轮生成完全相同的tool_calls直接中断并提示“检测到重复调用”。三是给工具结果多回传一些信息比如错误原因、可用参数范围让模型能更有针对性地修正。另外提到“Agent execution terminated due to error”这类报错常见的是上下文超长或者 API 返回 400。我遇到最多的是tool_call_id配对错误或者工具参数是非法 JSON。你可以在把工具结果回填前打印一下相关内容基本一眼就能找到问题。4.3 上下文越来越长后面效果越来越差Agent Loop 每转一轮messages 里就会多一条 assistant 消息和一条 tool 消息。如果任务复杂十几轮下来上下文会非常长既花钱又可能让模型“迷失在上下文中”。尤其是当你一次回传了大量工具结果模型再看后面的任务提示时注意力容易被带偏。我的做法是给工具结果做截断和摘要。比如某个工具返回 5 万字符我不会全部塞进上下文而是先截断前面几百字符或者用一个小模型做个摘要再把摘要回填。还有一个思路是只保留最近 N 轮的消息更早的历史可以折叠成一句“在此之前你已经完成了哪些步骤”让模型记得主干但不用看每一条细节。不过要注意截断得保守一点别把关键信息截没了。我一般在 debug 阶段先不截断跑通之后再逐步加截断逻辑避免引入新的问题。4.4 工具参数格式对不上函数执行报错模型生成的arguments是 JSON 字符串如果你用json.loads解析失败多半是模型生成了不合法 JSON比如多了一个逗号、用了单引号。这种问题在那个场景下很少见但模型越弱越容易出现。你可以加一层容错尝试json.loads如果失败就用正则匹配里面的纯字符串形式再不行就直接把原始字符串作为参数传给一个只接受单个字符串的工具。另一个常踩的坑是参数名不一致。比如你在 Schema 里写的是city函数定义里却写的是city_name模型按 Schema 生成{city: 北京}调用函数时就会报unexpected keyword argument city。最简单的办法是让函数参数名和 Schema 里的属性名保持一致别搞映射除非你有特殊需求。4.5 安全边界别让 Agent 随便执行一切工具最后提一个很多人忽略的问题Agent 能调用工具意味着它有“行动能力”如果工具里包含执行 Shell 命令、删除文件这种危险操作一定要做权限限制。从零实现的时候你可以先只让 Agent 调用只读工具比如查询天气、计算数学、读文件不要开放任意代码执行、数据库写入。如果确实需要写操作我建议加一个确认机制Agent 生成工具调用之后不直接执行而是先打印给用户看等用户输入 y 再真正执行。这个“人工审批”环节在自动化测试里尤其有用可以避免模型因为 prompt 注入或者理解偏差做出不可逆的操作。这也是为什么我把工具调用和真正执行分开设计表面上看多了几行代码但安全性和可控制性高很多。5. 从最小 Loop 到真正可用的 Agent记忆、技能与框架5.1 记忆给 Loop 加上持久化状态最小 Loop 里的消息列表就是记忆但它只是“当前任务内的短期记忆”。任务一结束消息列表清空什么都没留下。真实场景里Agent 需要长期记忆比如记住用户偏好、记住上次任务结果、跨会话复用经验。实现长期记忆的办法一般是用向量数据库存历史记录或者用结构化数据库存关键信息然后在每次任务开始前把相关的历史记忆检索出来拼进系统 prompt。我建议先不要急着上向量数据库你可以先做一个简单的文件版记忆每次任务结束后把关键信息存到一个 JSON 文件里下次任务开始时读取相关字段。这种方式足够让你理解“记忆”到底是怎么参与到 Agent Loop 里的。等你有经验了再换向量库也不迟。5.2 技能Skill和工具Tool的区别现在 Agent 社区里“Skill”这个词很流行。很多人问 skill 和 tool 到底有什么区别。我的理解是Tool 是原子操作比如“查天气”“计算表达式”“读文件”Skill 则是把多个 Tool 和提示词编排成一个“可复用的能力”比如“做一个市场调研”是一个 Skill它会内部调用搜索工具、读取网页工具、汇总工具甚至还会调用多个模型轮次。从最小 Loop 的角度看Skill 其实就相当于一个“复合工具”。你可以把 Skill 也定义成一个函数对外暴露统一的输入输出内部再去调用更小的工具。这样设计最大的好处是复用你不需要每次写任务都从零设计一堆工具调用流程而是把常见套路封装成 Skill。这也是为什么很多 Agent 框架都开始引入 Skills 的概念。5.3 框架到底在帮你解决什么当你理解了最小 Agent Loop 之后再去看现成的 Agent 框架会很轻松。框架解决的是工程化问题多工具调度、错误重试、并发执行、记忆插槽、可视化观测、插件体系、权限管理。这些不是核心原理而是规模化和稳定性的配套设施。你完全可以在自己的最小实现上一点点加但也确实没必要重复造轮子如果项目已经复杂到一定程度。不过我的建议是至少在动手做第一个 Agent 项目时自己把最小 Loop 手写一遍。这个“慢”会给你带来扎实的理解。以后再切换框架、选型技术方案你心里会有底得多。5.4 Agent Loop 的调试与可观测性最后一个我想强调的点是日志。Agent Loop 本身就是多步骤交互你如果不打日志出了问题根本没法查。我在实现里会在每个 Step 开头打印当前步骤数每次工具调用都打印工具名和参数每次工具返回都打印结果片段每次最终输出都完整打印。这不是为了炫技而是为了让整个循环过程“可审讯”。有条件的可以把每一步的消息都存下来后面复盘的时候看看模型是在哪一步开始“跑偏”的。很多 Agent 项目真正难的不是写代码而是调试。你要是能把日志做好后面排查问题会快很多。这也是我踩了很多坑以后最想分享的经验之一先把日志写好再谈优化。如果日志打印的是英文你可能会看到类似tool_call_id、role: assistant、finish_reason: tool_calls这种字段。别怕这就是最小 Loop 里最基本的几个元素。你只要把每一步的数据流动看清楚Agent 就不再是玄学。我个人在实际操作中的体会是手写一个最小 Agent Loop 最大的价值不是做出一个多厉害的 Demo而是让你建立对“模型输出驱动行动”这个机制的手感。工具调用的格式、上下文回填的顺序、停止条件的判定这些细节在文档里看一百遍都不如自己跑一遍印象深。最后再分享一个小技巧调试 Agent 循环时不要一上来就塞很多工具先从一个工具、一个任务开始跑通。跑通之后再加第二个工具再跑一个需要两个工具配合的任务。这样你很容易定位问题到底出在模型的工具选择上还是出在工具本身的执行上。等这个最小循环已经变得非常顺手你再去研究记忆、技能、多 Agent 协作会发现那些东西都是在这个核心 Loop 上长出来的。