资讯动态

5分钟从零手写AI智能体:核心循环与工具调用机制详解

发布时间:2026/10/19 10:51:17 来源:尧图企业网站定制
先泼一盆冷水别被“5分钟”“小白”“秒变大神”这些词冲昏头。智能体不等于“一个ChatGPT套壳”也不是“调个API就完事”。但我也能负责任地告诉你只要你掌握了正确的最小实现思路5分钟跑通一个能调用工具、能自主决策的Agent演示完全做得到。这篇文章不会教你去背那些玄乎的论文也不会扔给你一个三万行的框架源码而是用最直白的方式从零手写一个真正能干活的智能体讲清楚它背后的几个关键机理再把我踩过的坑一并交代清楚。先说结论一个最简的AI智能体本质就是“大模型 一组工具 一个循环”。大模型负责读任务、出计划、决定调哪个工具工具负责执行具体的动作循环则负责判断“任务是不是已经终结”。所有号称“Agent化”的产品真正核心的骨架就是这个循环剩下的记忆、规划、多角色协作全是围绕这个骨架长出来的增强功能。你只要把这个骨架钉死在自己脑子里再看任何Agent框架都是一层窗户纸。1. 先想清楚AI智能体到底是什么为什么大家都在做1.1 从大模型到智能体的本质跃迁如果你只用过大模型做“给它一段文本它回一段文本”那你知道的还只是“对话机器人”。对话机器人的特点是所有信息都存在于你的提问里所有回答只是语言生成。但智能体不一样它更像一个“实习生”你给一个模糊目标它需要自己拆解目标、查阅资料、操作工具、核对结果、调整计划然后交付一个结果。拿一个真实场景类比你和一个人说“帮我查一下最近两周线上订单的退货率并按城市排名”。对话机器人会怎么做它会直接编一个数据给你因为它的训练数据里根本不可能有你业务系统里的订单数据。智能体会怎么做它会先意识到“我没法直接访问你的业务库”于是决定调用一个“查询数据库”的工具填入查询SQL拿到结果后再调用“生成表格”的工具最后把排名结果整理成文字。这就是本质区别模型不再单靠记忆回答而是通过工具和环境交互用“动手”弥补“记忆”的不足。所以我一直认为判断一个系统是不是智能体就看三条它有没有调用外部工具的能力它有没有“任务未完成则继续执行”的循环它的路径是不是由模型动态决策的而不是写死的if-else。如果三条都满足哪怕代码只有100行它也是个智能体。如果三条都不满足哪怕界面再炫也只是个接口包装。1.2 谁适合读这篇以及你该怎么用它这篇文章最合适的读者有三类。第一类是后端或全栈开发者你已经会调用大模型API但现在想做点更有“自动化”感觉的东西想把Agent接进业务流程里。第二类是数据分析师或运维工程师你每天要处理大量重复性的查询、核对、报告工作想让一个Agent替你跑腿。第三种是纯小白你可能只会写一点Python甚至不是程序员但对大模型很感兴趣想知道“Agent到底怎么搭起来的”。对小白我说句实在话你不需要先啃完机器学习课程只需要会一点最基础的Python语法知道什么是函数、什么是循环、什么是字典就能跟上这里的代码。整篇的核心代码不到150行没有任何脏活累活。对开发者我更建议你别跳过前面的原理部分。很多人一上来就套LangChain或各类商业化框架结果报错都看不懂就是因为不理解Agent循环那几步在做什么。先把手写版弄明白再看框架你会觉得框架全是在替你写你本来就会的那几步。2. 5分钟跑通一个最小智能体2.1 环境准备与选型既然是“从零构建”我建议最少依赖。你只需要两样东西一个能调用的LLM API接口带消息对话能力和函数调用能力Python 3.9以上的环境再装一个requests库用来发HTTP请求。这里我不特意指名某家的API因为各家接口大同小异你用哪家的都行。你只需要把你的API密钥存到环境变量里别硬编码在代码里这是第一条安全底线。如果公司内部的模型服务那更好内网调用通常还免去网络问题。为什么要坚持“不依赖框架”因为我见过太多人学习Agent的方式是“先装个LangChain再看文档学着调”。那叫学会用别人的封装不叫理解Agent。我这边建议你至少手写一次把“模型输出字符串 - 解析工具名和参数 - 执行工具 - 拼接结果返回给模型”这条链路亲自打通之后再上框架你会发现自己能读框架源码了。提示如果你完全不会Python的dict、list、for循环先去补一下基础语法大约两小时。不补也能抄但调试时会痛苦。2.2 一个能跑的主循环我直接给出核心的Agent循环代码。这个循环是整篇文章的“心脏”。import requests import json import os # 你的模型API配置 API_URL os.getenv(LLM_API_URL, https://your-llm-service.example.com/v1/chat/completions) API_KEY os.getenv(LLM_API_KEY, ) HEADERS { Content-Type: application/json, Authorization: fBearer {API_KEY} } # 工具注册表所有可供Agent调用的工具都放这里 TOOL_REGISTRY {} def register_tool(): 装饰器把函数注册为Agent可调用的工具 def decorator(func): TOOL_REGISTRY[func.__name__] func return func return decorator def call_llm(messages, toolsNone): 调用大模型API返回完整响应 payload { model: your-model-name, messages: messages, tools: tools or [], tool_choice: auto } resp requests.post(API_URL, headersHEADERS, jsonpayload, timeout60) resp.raise_for_status() return resp.json()[choices][0][message] def run_agent(user_input, max_steps10): Agent主循环思考 - 调用工具 - 观察结果 - 继续或结束 messages [ {role: system, content: 你是一个智能体你可以通过调用工具来解决用户的问题。 如果需要工具请按指定格式返回工具调用否则直接给出最终答案。}, {role: user, content: user_input} ] tools [ { type: function, function: { name: calculator, description: 计算数学表达式比如加减乘除、幂运算等, parameters: { type: object, properties: { expression: {type: string, description: 数学表达式如 (12)*3} }, required: [expression] } } } ] for step in range(max_steps): print(f\n[Step {step 1}] 模型思考中...) message call_llm(messages, toolstools) # 情况1模型决定直接回答结束循环 if message.get(content) and not message.get(tool_calls): return message[content] # 情况2模型要求调用工具 if message.get(tool_calls): # 把模型这条回复追加到对话历史里 messages.append(message) for tool_call in message[tool_calls]: tool_name tool_call[function][name] args json.loads(tool_call[function][arguments]) print(f[Agent] 调用工具 {tool_name}参数: {args}) # 执行工具 if tool_name in TOOL_REGISTRY: tool_result TOOL_REGISTRY[tool_name](**args) else: tool_result f错误未找到工具 {tool_name} # 把工具结果返回给模型 messages.append({ role: tool, tool_call_id: tool_call[id], content: json.dumps(tool_result, ensure_asciiFalse) }) # 情况3什么都没返回视为异常 if not message.get(content) and not message.get(tool_calls): return 模型返回为空请检查API配置或输入内容 return 达到最大步数任务未完成。 register_tool def calculator(expression: str): 一个安全的最小计算器只允许数字和四则运算 import re # 只保留数字、运算符、括号、小数点挡住所有注入 if not re.match(r^[0-9\-*/().\s]$, expression): return {error: 表达式包含非法字符} try: result eval(expression, {__builtins__: {}}, {}) return {result: result} except Exception as e: return {error: str(e)}这个代码的运行流程你仔细看一遍模型每次回复要么输出最终文字要么输出一个“tool_calls”结构里面带着工具名和从文本里解析出来的JSON参数。你的程序负责执行工具把执行结果作为一条新消息塞回去模型看到结果后再决定下一步。这就是Agent最底层的动作逻辑模型出意图代码出执行力。模型永远不直接操作文件、数据库、网络它只是“提出操作请求”由你的代码执行。这个设计能最大程度避免模型乱改数据也方便你对每个动作做审计。2.3 让Agent拥有“触手”注册更多工具上面的代码里我用一个装饰器register_tool来注册工具。你可能觉得奇怪为什么不直接用if-else用过你就知道装饰器这种方式的好处是扩展工具时你不需要改动主循环加一个函数就是加一个能力。如果以后系统里面有100个工具主循环代码依然只有这几十行。我们立刻加两个最常用的工具查询当前时间和查用户信息。from datetime import datetime register_tool def get_current_time(format: str %Y-%m-%d %H:%M:%S): 返回当前时间可指定格式 return {time: datetime.now().strftime(format)} register_tool def get_user_info(user_id: str): 模拟查询用户信息比如用户的积分、等级 fake_db { U1001: {name: 张三, level: VIP3, points: 12345}, U1002: {name: 李四, level: 普通用户, points: 80}, } return fake_db.get(user_id, {error: 用户不存在})同时你需要把工具描述补充到tools变量里。这里要特别提醒工具描述写得好不好直接决定模型会不用这个工具。大模型没有读你代码的能力它只能看你的描述来决定何时调用。描述要尽量写清楚“这个工具在什么场景下用”“参数含义是什么”“返回什么格式”。我看到很多人的Agent“明明注册了工具但是永远不调用”八成就是工具描述太敷衍。另外工具的入参最好都用基础类型字符串、数字、布尔值。不要传嵌套对象。模型输出JSON参数时越简单越不容易出错。2.4 测试一下效果跑一下这个完整代码输入“帮我计算一下(12 15) * 8 等于多少顺便告诉我今天的日期”。你会看到类似这样的输出[Step 1] 模型思考中... [Agent] 调用工具 calculator参数: {expression: (1215)*8} [Agent] 调用工具 get_current_time参数: {format: %Y-%m-%d} [Step 2] 模型思考中... 结果(12 15) * 8 216今天的日期是2025-06-12。到这里你已经拥有一个“会用计算器、会看日期”的智能体了。整个过程如果不是网速慢确实可以在5分钟以内跑通。这个演示虽小但已经把“模型规划”和“工具执行”解耦得清清楚楚。3. 把“会说话”变成“会做事”工具调用的底层逻辑3.1 为什么必须让模型“调用工具”而不是把数据全塞进上下文可能有人会问既然模型能读文本我为什么不能把所有知识都写进提示词答案是三个字不现实。先算一笔账。假设你的业务数据库有一万条订单数据每条平均200个token全塞进上下文就是200万token。不说成本问题主流模型根本处理不了这么大的上下文。就算你的模型支持超长上下文让模型在几万条数据里自己“找”答案它很可能在计算中犯低级错误。模型本质是做统计推断的不是做精确检索的。工具调用的意义在于数据留在它该在的地方模型只负责发出查询指令。数据库查询、文件搜索、系统命令这些精确的活交给专业工具做模型只接收精准处理后的结果。这样既减少token又提升可靠性。比如上面计算器例子如果你不用工具而是让模型直接心算“(1215)*8”它可能算错一旦用计算器工具保证100%正确。我还想提醒一个安全视角工具调用让模型的行为变得“可审计”。模型的每一个动作都会留下一条明确记录“在哪一步、调了哪个工具、参数是什么、结果是什么”。这在需要合规审计的场景里极其重要。如果让模型直接Python解释器裸跑代码一旦提示词注入破坏力不可控而你用白名单工具每条工具都写清楚能干什么模型就算被恶意提示词诱导也只能调用预定义的能力。3.2 工具调用的三种主流形式你怎么选目前主流的大模型工具调用有三种实现形态你需要知道差别。第一种是原生函数调用就是上面代码里展示的。模型输出结构化的tool_calls你的代码分发给注册函数。优点稳定、省token、逻辑清晰。缺点模型需要支持函数调用能力老一点的模型不支持。第二种是代码解释器模式模型输出一段Python代码你的沙箱环境执行代码并把输出回传。优点表达力强能做复杂的数据分析缺点安全风险大必须有沙箱token消耗高。第三种是纯文本协议式模型输出一个固定格式的文本比如“工具名(参数)”或JSON你的代码用正则或解析器去解析。优点任何模型都能用缺点解析容易出错尤其遇到模型手滑多写个逗号的时候。我的建议是如果模型支持原生函数调用优先用原生如果不支持退而求其次用文本协议。代码解释器模式不推荐新手玩等你有完善沙箱了再说。3.3 写一个常用工具你会理解“工具边界”的价值再看一个更贴近业务的例子查订单金额。假设你有个本地CSV文件里面是订单数据。我们不希望模型直接读文件而是给它一个“查询订单总额”的工具。import csv register_tool def get_order_total(order_id: str): 根据订单ID查询订单总金额返回金额数字 with open(orders.csv, r, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: if row[order_id] order_id: return {order_id: order_id, total_amount: float(row[amount])} return {error: 订单不存在}你会发现这个工具把“文件内部结构”藏了起来模型只需要提供order_id就能拿到一个干净的结果。你完全不用担心模型去读CSV里的其他几百个字段导致上下文爆炸。这就是工具边界设计的意义把脏活、杂活、有权限差异的活封装成一个个“小接口”模型面对的是整齐的能力列表而不是原始数据海洋。现实里的Agent通常会有十几个到几十个这样的工具分别对接订单系统、库存系统、消息推送服务等。每一个工具背后都是一个微服务APIAgent的价值就是根据用户目标自主编排这些API的调用顺序。4. 记忆与状态管理让Agent“记得”说过的话4.1 短期记忆必须自己维护对话历史如果你只跑通了上面的代码你会发现一个问题Agent在一次任务的多个步骤之间是有“短期记忆”的因为我们一直在往messages里追加消息。但是一旦任务结束换个新问题如果还是用一个新的messages列表之前的对话就全忘了。这是很多新手最容易忽略的。很多人的Demo“好像没问题”一放到业务场景里用户说“刚才你算的那个数再帮我乘2”Agent直接懵了因为它根本没记住“刚才那个数”是多少。解决办法很简单在会话级把messages存起来。比如用Redis、数据库或内存里的dict以session_id为key保存整个消息列表。每次新用户消息进来从库里取出列表追加一条user消息再传给模型。sessions {} def get_session(session_id): return sessions.setdefault(session_id, [ {role: system, content: 你是一个智能体...} ]) def chat(session_id, user_input): messages get_session(session_id) messages.append({role: user, content: user_input}) ...这里的关键坑在token如果session无限长早晚会超出模型上下文窗口。所以一定要做消息裁剪。我常用的策略是只保留最初的system提示、最近N轮对话以及所有还没结束的工具调用。更激进的策略是把历史对话做摘要塞一条摘要消息进去。摘要方式适合长对话但会损失细节。注意裁剪消息时不要把“model的某条包含tool_calls的消息”单独删掉也不要把对应的tool结果删掉。两者必须成对保留否则模型看到tool结果却不知道是响应哪个调用或者看到一个调用却没有结果都会导致逻辑混乱。4.2 长期记忆用向量库做“可检索的记忆抽屉”除了短期对话历史Agent经常需要跨会话记住用户偏好、历史事实、业务知识。真正生产级的做法是向量数据库把已确认的事实或长期信息切片做向量化存储使用时按语义相似度召回。我不建议你在第一版就上向量库除非你已经有明显的知识检索需求。原因很简单向量库意味着你要处理文本切分、Embedding接口、相似度阈值设置复杂度一下子上去。前期完全可以先用一个简单的JSON文件或数据库表按用户维度存“关键事实”。比如用户告诉你“我是上海的用户经常买咖啡”你可以让Agent在对话中提取这类偏好写入用户档案表。下次开启新会话时把用户档案里的关键条目作为system消息的一部分传给模型。这个方案成本极低但在“个性化”层面的收益立竿见影。当然如果你要做一个“基于私有知识库问答”的Agent比如“帮工程师查询内部技术文档”那还是得用向量检索。上传文档 - 切片 - Embedding - 存向量库用户提问 - 向量化 - TopK召回相关片段 - 拼进上下文 - 模型总结。这一步的详细实现能写一整篇这里就不展开了。4.3 记忆管理的最佳实践别把整个历史都丢给模型我总结几个自己用的比较稳的经验。第一结构化记忆优于原文记忆。与其把用户上一轮说的十句话原样保存不如顺手抽取出“用户偏好咖啡、上海、预算单价30元以下”这种结构化档案。模型读结构化信息比读长对话更高效。第二定期做遗忘。记忆是会被噪声污染的用户随口提过一句“我下周可能去北京”两周后这个信息就没价值了。给记忆加一个时效字段过期就清掉。第三关键信息必须稽核。不要直接让模型把用户的话写进长期记忆库最好加一个“确认”环节如果模型想写入长期记忆先向用户复述确认避免幻觉数据污染记忆库。5. 踩坑实录从能跑到跑稳5.1 最常见的几个Bug百分之八十的新手都会遇到我把半年里见过的高频问题整理成一个速查表你如果遇到类似报错直接对照着排查。现象根因解决办法模型死活不调用工具直接编结果工具描述不清晰模型未启用函数调用开关工具参数名与描述不一致补全工具描述检查API参数打印tools内容人工核对报错tool_call_id不匹配工具结果返回时没有带上原始的tool_call_id或消息顺序被打乱确保每条角色为tool的消息的tool_call_id与对应tool_call.id一致工具参数解析失败JSONDecodeError模型输出的arguments不是合法JSON加一层容错先尝试json.loads失败则用正则抽取再失败就请模型重新输出Agent在同一个工具上反复调用形成死循环模型没有看到“最终目标已经达到”的判定条件在system提示里明确写“如果你已经获得足够信息请直接给出最终答案不要再调用工具”上下文越变越长很快触发token上限每轮都追加完整历史工具结果太长也被全量回传消息裁剪工具结果截断只保留前几百字符摘要历史API超时/限流并发过高或工具执行时间过长增加重试与退避工具执行加超时限制控制并发数第一条我特别想强调。很多新手一遇到“模型不调工具”第一反应是改代码其实九成是描述写得不够好。模型对工具的理解完全来自描述文本描述越具体调用越准确。比如get_current_time的描述光说“获取当前时间”可能不够最好带上“当用户问今天几号、现在几点、星期几时调用此工具”这样模型在意图识别时命中率会高很多。5.2 如何防止Agent“一本正经地胡说八道”模型幻觉是Agent落地最大的拦路虎。尤其在多步骤任务里如果某一步工具返回了个错误模型很可能脑补一个正确结果给你然后继续往下走。我的处理办法是在system提示里加上一条硬规则“工具的返回结果必须被如实引用不得修改或编造工具未提供的数据。如果工具返回错误直接向用户说明错误原因禁止猜测。”更稳的做法是给Agent加一个输出校验层重要事实必须来自工具结果模型生成的文字里如果包含了数字、日期、金额等关键字段程序可以反向检查这些字段是否在工具返回值中出现过。出现了才放行没出现就要求模型重新生成。这个方案虽然不能完美拦截所有幻觉但对业务场景已经足够有效。还有一个容易被忽略的点工具执行一定要加超时。如果工具去调第三方服务对方响应慢Agent的整个循环会被卡住。建议每个工具内部都设置一个独立超时时间一旦超时就返回“执行超时请稍后重试或换一种方式”。这样Agent至少不会无限等待下去。5.3 我实际调试中踩过的几个坑第一我在早期调试时为了让模型“多用工具”在system提示里写了“你应该尽可能调用工具”结果模型把“查时间”这种简单事也非要调一次get_current_time搞出一堆无用调用。后来改成“只有必要时才调用工具能直接回答就直接回答”调用次数立刻降下来了。工具调用是手段不是目的设计提示词时要把这个优先级讲清楚。第二关于工具参数的默认值。模型给出的JSON参数有时候会漏掉非必填字段比如get_current_time(format...)。如果你用Python的**kwargs直接传给函数缺字段可能报“missing required argument”。我的方案是定义工具函数时全部给默认值或者在函数内部做一次参数补齐。宁可让工具“宽松一点”也不要因为参数缺失让整个循环崩掉。第三日志是Agent开发的第一生产力。我在开发时每一步都打印完整的messages内容、模型返回的raw JSON、工具执行耗时。因为Agent的失败往往不是一次能定位的可能是某个步骤模型理解错了、某个参数变形了。这时候没有完整日志你只能靠猜。所以我把话放在这里不打印完整中间过程的Agent开发等于闭着眼睛开车。6. 进阶扩展从Demo到可用产品6.1 让Agent学会拆任务Plan-and-Execute模式最小演示只能算“单步工具调用”。真实任务往往需要多步规划。比如用户说“统计本月销售额和上月对比生成一份简要分析报告”这就不是一个工具能搞定的。一种很实用的升级方式是Plan-and-ExecuteAgent在收到目标后先不急着执行而是列出三个环节的计划比如“第一步查询本月销售额第二步查询上月销售额第三步比较并生成报告”。然后按计划逐步执行。实现上只需把循环改造成“先调一次模型专门输出计划再逐计划调用工具每完成一步更新状态”。这种模式在复杂任务里比“模型自由发挥”稳定得多缺点是更耗token、更慢。但你要警惕“过度规划”有些Agent会列出五六步计划其实两步就能完成。解决方案是给计划加一个“必要性校验”或者让模型“最少步骤优先”。我在实践中倾向于让模型先判断“解决这个问题最少需要几个工具”再根据最少步骤制定计划效果比单纯让它“尽量详细”好很多。6.2 构建评估集与回归测试别让自己被Demo欺骗很多人做Agent Demo时觉得“跑通了”上线跑了一周才发现昨天还能完成的流程今天模型更新后突然不会调工具了或者某个Prompt改动后原来能通过的用例全崩了。这就是缺少回归测试的代价。我在项目里会维护一张“黄金测试集”大概20到50条用例覆盖不同意图和复杂度。每次改动代码或提示词都会跑一遍测试看看成功率有没有下降。test_cases [ {input: 计算 123*456, expect_tool: True}, {input: 今天是几号, expect_tool: True}, {input: 介绍一下你自己, expect_tool: False}, {input: 查询用户U1001的积分, expect_tool: True, expect_keyword: 12345}, ]跑测试时不仅要看最终结果对不对还要看工具调用次数是否合理。比如一条简单问题调了五次工具哪怕结果对也说明提示词有冗余。把这些指标纳入“质量门禁”你才能保证Agent在持续演进中不坏掉。6.3 降本增效的三个实用策略Agent比普通对话调用API的次数更多因为每个任务要多次和大模型交互。所以成本问题必须提前考虑。我的经验是三部曲。第一模型分级。简单工具调用和分类任务用便宜的小模型复杂规划、报告生成用强大的大模型。Agent内部可以加一个“意图复杂度评估”环节先让模型用一句话判断任务难度再决定走哪条模型通道。第二缓存高频结果。对于“查今天天气”“查某订单状态”这种重复查询如果工具结果是确定性的可以加一个短时缓存不用每次都让Agent走完整循环。第三减少无效工具调用。每次工具调用都消耗一轮模型交互所以工具描述里要能帮模型过滤掉大量“不可能需要的工具”。工具数量超过二三十个时可以考虑先让模型从工具列表里检索“候选工具Top5”再基于少量高相关工具做规划能省不少token。写给你的一点真心话从跑通第一个手写Agent到现在我最深的体会是Agent的难点从来不在代码而在“如何恰当地约束和引导模型”。模型的能力很强但它没有常识、没有边界感它会老老实实按你的描述去调用工具也会在你描述不清时自由发挥给你编一个结果。所以真正的Agent开发功夫全在工具设计、提示词约束、日志观测和回归测试这些“看起来不酷但决定生死”的事情上。如果你刚跑通自己的第一个Agent我建议你做的下一个实验是给它接上三个真正的业务工具设计一个跨工具的复合任务让它独立完成。你会亲眼看到模型“计划-执行-纠错”的全过程。那一刻你才算真正从一个会写代码的人变成一个会驾驭智能体的人。剩下的事情就是反复暴露问题、反复修边界直到它成为一个让你敢放心交活的搭档。

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

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

免费获取报价 →
↑