从零手写大模型Agent:工具调用与任务规划实战指南
1. 先说清楚Agent到底是什么做开发这些年你会发现一个特别有意思的现象三年前大家见面聊的是“你的系统用了什么框架”两年前聊的是“你的模型推理速度多少”现在见面聊的基本都是“你的Agent能干活了没”。大模型Agent开发说白了就是让大模型不再停留在“你问我答”的聊天阶段而是变成一个能自己拆任务、调工具、跑流程、交付结果的数字员工。我见过不少朋友第一次接触这个概念时以为Agent就是写个prompt套壳实测下来发现完全不是这么回事。一个合格的Agent背后至少牵扯到模型选择、工具注册、任务编排、上下文管理、结果校验这几个环节任何一个环节掉链子整个流程就卡住。这篇文章适合谁看如果你已经用过ChatGPT或国内的大模型API会写Python基础语法但对“Agent怎么架构”“工具调用怎么实现”“多步任务怎么拆解”还没什么概念——那这篇文章就是给你准备的。我会用自己踩过的坑和跑通的代码把整个链路掰开揉碎讲一遍。不夸张地说跟着走一遍你就能搭出第一个真正能干活的Agent而不是一个只会背prompt的玩具。2. 设计思路Agent的本质是把“思考”和“行动”连起来2.1 为什么单单靠Prompt不够用很多人以为Agent就是把Prompt写长一点、写详细一点模型就能自动完成任务。早期我也这么干过结果发现模型答得挺好但它不干活。举个例子你让大模型“帮我把这个文件夹里的图片都压缩一下”如果只是Prompt对话模型会给你输出一段Python代码然后告诉你“你把这个代码跑一下就行”。看起来没问题但这不是Agent这是只会动嘴的方案顾问。真正的Agent需要自己去遍历文件夹、调用压缩工具、检查输出结果、失败自动重试——这些动作必须通过代码来执行Prompt只是大脑手脚得另外接上。所以Agent开发的核心思路就一句话把大模型当作“指挥官”给它配备一堆“工具”再把“指挥官”的决策循环跑起来。这个循环通常是观察任务 - 思考拆解 - 调用工具 - 观察结果 - 继续决策直到任务完成或达到终止条件。2.2 工具调用到底是怎么回事工具调用的机制值得多说两句。你要让模型调用工具不是把函数文档一股脑塞给模型就行。大模型的API里有一个专门的机制叫“函数描述”Function Calling或“工具定义”Tools Definition你需要把每个工具的名字、参数、用途、返回格式用结构化的JSON描述出来随消息一起发给模型。模型收到这些描述后在回答时不会直接说“我去调用工具了”而是返回一个结构化的指令比如“我想调用search_web这个工具参数是keyword: 北京天气”。你的代码收到这个指令后去真正执行搜索再把搜索结果作为一条消息返回给模型。模型看到结果后继续思考下一步。这个“模型决策 - 代码执行 - 结果回传”的循环就是Agent区别于普通对话的核心机制。一开始你可能觉得绕习惯了之后你会发现这个设计特别优雅——模型的思考和你的代码各管一摊模型负责判断代码负责干活。2.3 框架选型从零手写还是用现成框架关于框架我的建议是先别急着上LangChain这种重框架先用原生方式手写一个最小可用的Agent把调用循环吃透了再说。因为框架虽然省事但把太多细节藏了起来一旦出现问题光排查框架的封装逻辑就能折腾你一整天。等你搞明白了底层原理再用LangChain、Dify、Coze这类框架或平台提效就是锦上添花的事。我自己就是先手写了个两百多行的Agent跑通全流程后来切到LangChain时看它的文档跟看老朋友似的一点不费劲。你如果一上来就用框架很容易陷入“会用但不懂”的尴尬境地。后面遇到复杂需求需要定制改造时你会发现自己完全没有头绪。3. 实操准备环境、API选型与模型选择3.1 开发环境搭建开发Agent不需要太复杂的环境一个Python版本管理器、一个虚拟环境、一个API密钥就够了。我目前用的配置供你参考Python 3.10太老的版本有些新库装不上使用 venv 创建独立虚拟环境避免依赖冲突在根目录建.env文件存放API密钥绝不硬编码在代码里开发调试阶段使用requests库直接调HTTP接口暂时不需要引入SDK这里有一条实操心得很重要不要把API密钥提交到Git仓库。我见过不少同事把密钥写在代码里直接push上去结果被爬虫扫到一夜之间被刷了几百块费用。最稳妥的做法是.env文件加入.gitignore加载环境变量用python-dotenv这个库非常方便。3.2 模型怎么选大小结合才是正经路数大模型选型这块新手最容易犯的病是“只用最强的模型”。GPT-4级别的Token价格摆在那你让Agent做一次网页搜索加分析一个任务来回十几轮调用成本嗖嗖往上蹿。我的建议是大小模型搭配使用规划、决策、复杂推理阶段用强模型比如GPT-4级别或Claude系列的中高端版本因为这一步错了后面全错值得花高价提取、总结、格式化输出这类机械性环节用小模型或低成本模型完全够用速度快、价格只有大模型的十分之一甚至更低模型选择的另一个关键指标是“上下文长度”。上下文长能喂进去的历史消息就多Agent翻车率会明显降低。目前主流模型已经支持几十万Token级别的上下文但Token越长、每次请求的延迟和价格就越高建议你把系统提示词压到500Token以内日常工作消息控制在2000Token左右就足够了。3.3 API网关别把鸡蛋放在一个篮子里做Agent开发一段时间后你就会发现任何单一模型的API都可能出现限流、故障、响应变慢的情况。我的建议是搭一个轻量级的API网关把不同厂商的接口统一封装可以随时切换避免被单一供应商绑死。特别是国内环境不同厂商的模型各有所长有的在中文理解上更强有的在代码生成上更胜一筹。我用Python写了个简单的封装层核心逻辑是给每个模型接口配置一个统一的数据格式再接一个路由函数专门用来做模型切换和异常回退。实测下来一套接口适配了全系模型再也不用针对每个厂商单独写一套代码。4. 手写一个最小可用Agent完整跑通全流程4.1 最基础的消息循环我们直接上手写代码。第一步是让模型具备基本的对话能力构建一个“说话——听话——回话”的循环。import os from dotenv import load_dotenv import requests load_dotenv() API_KEY os.getenv(API_KEY) API_URL os.getenv(API_URL) # 比如 OpenAI / 国内大厂的兼容接口地址 def call_model(messages, toolsNone): payload { model: gpt-4o-mini, messages: messages, tools: tools, tool_choice: auto, } resp requests.post(API_URL, headers{Authorization: fBearer {API_KEY}}, jsonpayload) resp.raise_for_status() return resp.json()[choices][0][message] def run_agent(user_input, max_iterations10): messages [{role: system, content: 你是一个可调用工具的智能助理。}] messages.append({role: user, content: user_input}) for _ in range(max_iterations): assistant_msg call_model(messages) messages.append(assistant_msg) # 如果没有工具调用请求说明任务完成直接返回 if not assistant_msg.get(tool_calls): return assistant_msg[content] return 超出最大迭代次数任务未完成。 if __name__ __main__: result run_agent(你好介绍一下你自己) print(result)这段代码虽然简单但已经具备了Agent最核心的循环骨架。注意迭代次数的限制很关键这是用来防止Agent陷入死循环的安全阀。我曾经没设这个上限结果有一次模型反复调用同一个工具连续跑了二十多轮不仅慢还烧了不少Token。从那次之后我所有的Agent工程都会强制加上迭代上限。4.2 把工具接进来让Agent真正开始干活光有对话能力远远不够。我们给Agent接入一个计算工具让它能做数学运算。def calculate(expression): 安全计算器只处理数字和四则运算防止代码注入 allowed_chars set(0123456789-*/(). ) if any(c not in allowed_chars for c in expression): return 错误表达式包含非法字符 return str(eval(expression)) TOOLS [ { type: function, function: { name: calculate, description: 计算四则运算表达式的结果只支持 - * / 和括号, parameters: { type: object, properties: { expression: {type: string, description: 要计算的数学表达式} }, required: [expression] } } } ]有了工具定义之后在run_agent函数里把toolsTOOLS传进去然后需要处理模型返回的tool_calls字段。def execute_tool_call(tool_call): if tool_call[function][name] calculate: args json.loads(tool_call[function][arguments]) return calculate(args[expression]) return 未知工具 def run_agent_with_tools(user_input): messages [{role: system, content: 你是一个可调用工具的计算助理。}] messages.append({role: user, content: user_input}) for _ in range(10): assistant_msg call_model(messages, toolsTOOLS) messages.append(assistant_msg) if not assistant_msg.get(tool_calls): return assistant_msg[content] for tool_call in assistant_msg[tool_calls]: result execute_tool_call(tool_call) messages.append({ role: tool, tool_call_id: tool_call[id], content: result }) if __name__ __main__: result run_agent_with_tools(帮我计算 (123 456) * 78 的结果) print(result)这一步非常关键你把工具结果用role: tool回传给模型模型才能“看到”工具执行的结果并基于结果进行下一步决策。这个来回如果格式不对模型会直接报错所以常见错误里我会详细讲怎么排查。4.3 工具箱扩展AI能力百宝箱思路当你把计算器接好之后后续要扩展其他工具思路是完全一样的。接下来是我在项目里用到的高频工具供你参考网络搜索工具调用搜索API让Agent能查实时资讯和网页内容网页抓取工具输入URL返回网页正文文本方便Agent做内容分析数据库查询工具让Agent能查自己的数据表做数据分析文件读写工具实现文件的操作能力方便输出报告或读取本地配置图片生成工具接入绘图API让Agent具备多模态能力定时提醒工具让Agent跟日历和消息推送联动起来每加一个工具只需要写一个普通Python函数再加一段JSON描述然后写一个分支让模型调过来就完事了。所以Agent的开发工作量真正核心不是代码量而是“你想让Agent具备哪些能力”的需求设计。4.4 让Agent能“规划”拆解复杂任务必须走的一步再往上走一步是给Agent加上任务规划能力。所谓规划本质是把复杂任务拆解为多个子步骤然后按顺序执行。实现思路有几种有的是用提示词要求模型“先列计划再执行”有的是用ReAct框架让模型交替进行“推理”和“行动”。我推荐一个轻量级的方案先做“规划器”再做“执行器”。两个角色共用同一个模型但消息上下文各不相同。PLANNER_SYSTEM 你是一个任务规划器请把用户的任务拆解成最多5个具体步骤每个步骤一句话描述用编号列表输出。 EXECUTOR_SYSTEM 你是一个任务执行器你只能执行当前这一步执行完成后如实汇报结果。这种拆法就是经典的“规划器——执行器”架构Planner-Executor。好处是任务拆解和执行互不干扰规划做得不好时也不用把执行上下文全部推倒重来。更复杂的任务还可以引入“反思”模块让模型在执行完一轮之后回看一下前面的步骤有没有问题再决定是继续还是调整方案。这就是OpenAI在相关技术报告里展示过的类似设计思路许多企业级Agent已经把这类机制发展得挺成熟了。5. 开发过程常见的坑与经验排查5.1 工具描述写得太含糊导致调度失误这一个大坑相信每个做过Agent的人都会踩到。模型面对多个工具时如果工具描述写得不清楚它根本不知道该选哪个。比如你写了“search”工具描述是“搜索一下”模型可能不知道它是搜网页还是搜数据库调度就乱了。后来我的做法是每个工具的描述必须包含以下三块信息这个工具“做什么”说得越具体越好什么场景下“应该用”或“不应该用”关键参数的含义和限制举个直观对比差的描述获取天气信息好的描述获取指定城市当前和未来的天气信息适用于用户询问天气、出行建议、活动安排等场景。如果不确定城市名先向用户确认。城市参数使用中文全称。同样的工具函数描述写清楚了模型的准确率能提升很大一截。这成本很低但效果是立竿见影的。5.2 上下文越聊越长管理策略从清理开始Agent每走一步多轮对话的上下文就会越来越长。消息太多之后Token成本上升模型响应变慢还会导致模型被一些无关历史干扰出现“幻觉”式的错误决策。我常用的策略有几种只保留最近N轮对话的工作记忆区存储更早的历史做摘要对于工具执行结果只保留结论和重要数据不保留完整原始返回对系统提示词每隔一段时间就重新发送关键的指令避免模型在长对话中“遗忘”系统设定这就像你平时整理工作台桌子堆满了就难找东西但你不能把文件全扔了只能把不常用的归档、常用的放桌面。Agent的上下文管理逻辑完全一样。5.3 Token消耗过高的问题排查与优化Token烧钱的问题其实不太难解决关键是找出钱都花在哪了。每次调用如果传入了完整的工具定义这些描述本身就要消耗Token。你定义了几十个工具每次请求都带上消耗是很大的。我的优化方案是“分组动态加载”把工具按功能分成几组根据用户的请求让模型先判断走哪个场景再只加载对应组里的工具。做法是先用轻量级的收分类接口识别用户意图经过这样一次预筛再根据意图拼接对应的工具子集传给模型。这种方式在做了工具数量大于10个的场景后Token消耗能明显降下来响应速度也快了不少。如果你手上的Agent特别重这个思路可以优先考虑。5.4 排查问题从Log驱动到流程追踪Agent开发最难的环节是问题排查。因为Agent的每一步决策、工具调用、反馈回传都是动态的中间任何一环出错结果是多样的。有几次模型没按预期行动我盯着终端看了半天也看不出毛病。后来我做了个决定给Agent开发加上完整日志记录功能每轮消息、工具调用参数、返回结果、Token消耗全部落盘。这样做的好处是Agent出了问题之后回看日志基本上能秒定位。日志记录看起来不复杂但它是Agent开发里的隐形基础设施强烈建议从第一天就做起来。我记得有个同事跑Agent任务时遇到连续失败的诡异情况排查了整整一天没头绪。后来看了日志发现是在某次工具调用返回时多了一个空字符串导致模型在后续步骤里把空结果当成有效数据一路推理到错误方向去了。这就是日志的价值——它不直接告诉你答案但能把你的排查范围缩到很小。为了让日志更有用我一般还会在每条工具信息里加一个调用耗时方便判断瓶颈到底在模型侧还是在API请求侧。6. 进阶方向从单Agent到多Agent与安全思考6.1 多Agent架构是怎么一回事单Agent能解决很多问题但遇到复杂业务场景时把多个Agent组合在一起会更灵活。我的经验是用“主控Agent 若干专业Agent”的分工模式主控Agent只负责理解用户意图拆解任务然后分发给背后的专业Agent去执行。比如说一个企业知识库客服系统可以拆成意图识别Agent快速判断用户问的是技术问题、售后问题还是售前咨询技术答疑Agent对接产品文档回答远程配置、系统报错等具体问题订单处理Agent查订单状态、处理退款、对接售后流程情绪安抚Agent升级突发事件用共情的话术稳住客户并及时转人工处理不同Agent之间通过消息队列或共享状态通信。消息传递的协议设计如果做得好整个系统会特别稳。多Agent之间有时也会“吵架”——两个Agent的意见不一致这时候就需要一个仲裁机制或者干脆让主控Agent重新判断。6.2 Agent安全怎么考虑Agent安全是最近行业里讨论非常多的话题。Agent拥有了工具调用能力之后权限边界如果控制不好风险比普通对话应用大得多。核心坏例子就是Prompt注入攻击外来的恶意内容伪装成了系统指令诱导Agent去执行高危操作比如发邮件、转账、删除数据。我在自己的项目里养成了一个习惯重要操作一律走“二次确认”机制。Agent要执行高危动作时先停下来向用户确认用户点头了才继续。除此之外敏感操作的执行权限要与模型决策权限分离——Agent能调用的API密钥必须是最小权限的那一个。不能让一个客服Agent拿着服务平台的全部API权限。这些安全基操是保证Agent能真正投入生产环境的前提。6.3 我的实操体会与选择思路如果让我说一条最想给新手分享的心得那就是先定义清楚“边界”再动手写Agent。有很多人上来就堆功能、堆框架、堆模型结果做出来的东西在外人眼里很高级实际大家都看到了——工作不稳定、结果不可控、上线之后根本不敢让用户随便用。Agent开发最本质的思维方式就是“把大模型放对位置”。大模型是发动机不是整车。你要给它配好方向盘、刹车、仪表盘它才能安全地跑起来。想清楚这一点之后你跟大模型配合的每一次决策都会变成尽可能清晰、可控、可追溯的执行链路。有几次我凌晨还在改代码就是因为模型在某个边界条件下做了我没预料到的操作。后来我把所有“兜底逻辑”前置把能设上限的地方全部设了上限把能加确认的操作全部加确认系统稳定性肉眼可见地提升了。做Agent开发慢慢你会发现最有价值的代码都是那些不起眼的防御代码和不显眼的日志代码。这条路越往深走越有意思多模态、多Agent协作、长期记忆、持续学习每一个方向都值得慢慢折腾。工具不停在变但核心的架构思想和工程习惯会比任何单个工具都保值。