AI Agent这个词快被说烂了但我在不少技术社群里观察到一个现象聊概念的人很多真正动手把Agent跑起来的人却没几个。大多数教程要么堆理论要么甩一堆又长又重的框架代码小白看完直接劝退程序员想改造又不知道从哪下手。这篇实战指南不玩虚的我带你从零开始搭一个真正能工作的LLM Agent智能体代码量不大逻辑也直白把模型、工具、记忆、循环这几根骨架一次讲清楚。不管你是完全没写过代码的小白还是写了几年业务代码的程序员只要跟着这篇文章走一遍你就能拥有一个可复现、可扩展的最小Agent——它能够理解你的自然语言指令自己决定调用什么工具、执行什么动作最后把结果组织成人话回复你。做出来的东西还能继续接上真正的业务系统当玩具玩也行当入门样板也行。1. 先聊清楚AI Agent到底是个什么东西1.1 Agent和普通对话机器人差在哪很多人的第一个误区就是把“能聊天的AI”当成Agent。你要是问“今天天气怎么样”普通聊天机器人给你一段通用回答就结束了Agent则会判断这句话有明确的查询意图主动去调天气接口拿到实时数据基于数据告诉你“今天晴转多云建议带伞”。区别在于普通聊天是“一次性问答”Agent是“目标驱动的多步执行”。我给小白解释时常用的类比是普通聊天像问路对方给你指个方向你们之间就结束了Agent像一个代办跑腿的他听到你的诉求后会自己规划路线、出门绕开修路路段、确认目的地、最后回来把结果交给你。你只需要表达“我想做什么”剩下怎么拆解、怎么执行是Agent的事。所以判断一个东西是不是Agent有个很简单的标准它有没有在一轮交互里自主地做出一连串决策和动作。如果只是把用户的话喂给模型、返回一段话那它还是“聊天框”不是Agent。1.2 一个Agent的五件套拿一个能用的Agent拆开看基本是以下五块拼起来的意图理解与任务规划把用户输入拆成可执行的步骤回答“用户到底要什么、先从哪一步下手”。工具调用层也就是Agent的“手脚”通过API、函数、数据库查询完成实际操作。没有它Agent就只剩一张嘴。记忆管理短期记忆是上下文消息长期记忆用来沉淀用户偏好和历史事实没有记忆就聊两句就失忆。决策与反思每一步执行后检查结果发现不对就换方案重来这是Agent能“修正错误”的关键。主循环把上面几样串起来不断进行“推理-行动-观察”的循环是整个Agent的运转底盘。模块解决什么问题没有它会怎样常见实现方式意图理解与规划用户到底要什么、第一步做什么只能回答不会做事大模型prompt编排工具调用怎么执行具体动作只会纸上谈兵函数调用、工具注册表记忆管理上下文怎么存、怎么捞聊两句就失忆消息列表、向量数据库决策与反思失败了怎么办一步错步步错循环重试、错误信息回传模型主循环怎么组织整个流程模块散落不成系统ReAct模式的循环代码这里要特别说一点很多人以为只要给模型加了一个tools参数它就成了Agent。其实那只是工具调用能力真正的Agent重点在主循环——它让模型像人一样做完一步看一眼结果再决定下一步。这也是本文后面代码里最核心的东西。2. 动手前要做的三个决策模型、框架、工具很多人一上来就写代码结果三天后卡在“为什么模型不按我的要求输出”。动手之前先把三个决策想清楚后面能少走一半弯路。2.1 大模型选型按场景挑不是越贵越好大模型是Agent的大脑选型直接影响后续开发难度。我不对比具体厂家只讲选择维度上下文长度决定了Agent一次能记住多少历史。聊天场景至少8K起步复杂工作流建议32K以上不然几个来回就把窗口撑爆了。工具调用function calling能力这是Agent最看重的模型要能输出结构化的工具调用参数而不是只会写解释文字。挑模型时拿一个“让模型调用计算器工具并返回结果”的测试集先跑一遍比看榜单分数更有用。延迟与成本Agent一轮任务往往要调用多次大模型等于一次任务要多次计费不能只看单次价格。中文与任务类型的适配同样是指令跟随不同模型在中文表达、格式输出上的表现差别很大最好拿自己的真实任务做验收。我整理了一张选型策略表方便对号入座使用场景推荐方向理由个人学习、快速验证闭源大模型API选便宜的轻量版开箱即用零部署成本试错最快公司内部工具、数据敏感开源模型本地部署数据不出内网但要自己配推理环境高并发生产服务闭源大模型API 缓存层弹性好运维成本低离线或边缘设备小参数量开源模型牺牲效果换速度和隐私我的建议是第一次搭Agent优先用能随手调用的API版本哪怕贵一点也值得。先把业务逻辑和产品效果跑通再去考虑换便宜模型、本地部署这些事。否则一上来就调开源模型你根本分不清问题是出在模型能力上还是出在Agent框架上。2.2 框架选择现成Agent框架 vs 手写最小实现市面上的Agent框架五花八门有通用的也有专用的。好处是功能齐全不用自己写工具注册、记忆、多Agent编排这些基础设施。但问题也明显框架迭代太快文档和版本对不上封装太深报错时你根本不知道是哪一层出了问题。我个人有个原则学习阶段手写一个最小实现。代码量不过两三百行但每个环节都是自己可控的出了问题能立刻定位。有了底层理解再去看那些框架你会发现它们无非是“工具注册表 主循环 记忆”的豪华定制版那些抽象概念瞬间就落地了。这篇文章接下来就带你手写。真实项目中如果你嫌麻烦可以拿手写的这个骨架做底座慢慢替换或引入组件不必迷信框架。2.3 工具设计Agent的“手脚”长什么样工具设计是很多人忽略、但非常影响成败的一环。所谓工具本质上就是一个被包装成大模型能理解的函数。有两个关键点第一工具注册表要提供清晰的schema结构描述。大模型靠这个schema来决定何时调用工具、传什么参数。一个典型的工具schema长这样{ type: function, function: { name: calculator, description: 计算数学表达式例如 3.14 * 5 2只支持四则运算和小括号, parameters: { type: object, properties: { expression: { type: string, description: 需要计算的数学表达式 } }, required: [expression] } } }这个JSON里最重要的不是字段格式而是description写得好不好。模型能不能正确调用工具六七成取决于描述是否清晰。你写“计算算术表达式”和写“计算数学表达式例如‘3.14 * 5 2’只支持四则运算和小括号”后者的调用准确率会高出一大截。原因很简单模型是通过自然语言理解工具能力的描述越明确判断越准。第二工具的结果要做统一封装。不管成功还是失败都建议返回一个结构化的JSON至少包含status、data、error这几个字段。特别注意工具本身报错的信息一定要原样交给模型。很多新手在这里吃亏工具抛异常调用方捕获异常后直接吞掉模型最后只看到“调用成功”就会顺着错误结果继续编造答案。这个坑后面我会专门再讲。3. 从0到1手写一个最小可运行的Agent到了全篇核心。我会先准备环境再写两个工具最后把主循环串起来。你照着敲就能得到一个能在命令行里对话的Agent。3.1 环境准备与依赖安装硬件上不需要特殊设备一台能联网的电脑就够。软件方面准备这些Python 3.10及以上版本一个能调用大模型API的Python SDK文中用llm_sdk代指你用自己选定的SDK即可可以用pip直接安装的依赖llm-sdk、python-dotenv建议用虚拟环境别把依赖装到系统全局。命令如下python -m venv agent-env source agent-env/bin/activate pip install llm-sdk python-dotenv装完之后找一个地方存放你的API Key。我习惯放到项目根目录的.env文件里然后让代码统一从环境变量读取。硬编码密钥不仅不安全还容易误传到公开仓库这个习惯建议从第一天就养成。3.2 先给Agent装上两只手计算器与待办清单工具我们造两个最实用的工具来演示Agent怎么工作一个安全计算器一个待办清单管理器。先看安全计算器。为什么不直接用Python内置的eval因为eval会执行任意代码你不想让Agent传一句“删除文件”就真的执行删除逻辑。所以要做白名单校验只允许数字、四则运算、括号这些基本元素。import ast import operator _ALLOWED_OPS { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, ast.Mod: operator.mod, ast.Pow: operator.pow, } def safe_eval(expr: str) - float: 只允许数字和四则运算的求值器防止任意代码执行。 tree ast.parse(expr, modeeval) return _eval_node(tree.body) def _eval_node(node): if isinstance(node, ast.Expression): return _eval_node(node.body) if isinstance(node, ast.Constant): if isinstance(node.value, (int, float)): return node.value raise ValueError(不支持的常量类型) if isinstance(node, ast.BinOp) and type(node.op) in _ALLOWED_OPS: left _eval_node(node.left) right _eval_node(node.right) return _ALLOWED_OPS[type(node.op)](left, right) raise ValueError(不支持的表达式语法)这块看着有点底层但对新手很友好它把Agent能操作的指令限制在一个极小的集合里。以后接真实业务工具也是同样的思路只暴露你期望Agent能动的接口其余一律不给。再来看待办清单工具。我用一个JSON文件做持久化这样每次运行都能读到上次的数据不用上数据库。import json import os TODO_FILE todos.json def _load_todos(): if not os.path.exists(TODO_FILE): return [] with open(TODO_FILE, r, encodingutf-8) as f: return json.load(f) def _save_todos(todos): with open(TODO_FILE, w, encodingutf-8) as f: json.dump(todos, f, ensure_asciiFalse, indent2) def add_todo(task: str) - dict: todos _load_todos() todos.append({task: task, done: False}) _save_todos(todos) return {status: success, data: {total: len(todos)}} def list_todos() - dict: todos _load_todos() return {status: success, data: todos}注意这里的返回格式都是dict并且包含status。后面主循环拿到之后会把这个dict序列化成JSON字符串塞回对话上下文。工具准备好了还要把这两个工具的schema写出来供模型读取。calculator工具的schema我前面已经展示过待办工具的schema思路完全一样只是name换成add_todo/list_todos参数换成task或者用空对象表示不需要参数。写的时候记得把“怎么用、输入什么格式、返回什么”写具体。拿不准时宁可多写几个字把触发条件写明白实测下来工具说明写详细的调用准确率能提升一大截。3.3 让大脑转起来Agent主循环的实现前面的工具像两只手主循环就是连接大脑和手的骨架。它的工作流程可以概括成一句口诀把历史、工具说明一起丢给模型如果模型说要调工具那就执行工具并把结果送回给模型如果模型说不需要工具了那就把最终回答展示给用户。直接上核心代码import json import os from dotenv import load_dotenv from llm_sdk import LLMClient # 代指你使用的SDK load_dotenv() client LLMClient(api_keyos.getenv(LLM_API_KEY), modeldemo-chat) system_prompt 你是一个可靠的智能助手可以调用工具完成任务。如果工具调用出错就根据错误信息重新尝试。 messages [{role: system, content: system_prompt}] # 合并工具schema TOOLS [ { type: function, function: { name: calculator, description: 计算数学表达式例如 3.14 * 5 2只支持四则运算和小括号, parameters: { type: object, properties: { expression: {type: string, description: 需要计算的数学表达式} }, required: [expression] } } }, { type: function, function: { name: add_todo, description: 添加一条待办事项, parameters: { type: object, properties: { task: {type: string, description: 待办事项的内容} }, required: [task] } } } ] # 工具分发 def call_tool(name: str, arguments: dict): if name calculator: try: value safe_eval(arguments[expression]) return {status: success, data: value} except Exception as e: return {status: error, error: str(e)} if name add_todo: return add_todo(arguments[task]) return {status: error, error: funknown tool: {name}} MAX_STEPS 5 while True: user_input input(你) if user_input.strip().lower() in (q, quit, exit): break messages.append({role: user, content: user_input}) for _ in range(MAX_STEPS): response client.create(modeldemo-chat, messagesmessages, toolsTOOLS, temperature0.3) assistant_msg response[choices][0][message] # 1) 模型想调工具 if assistant_msg.get(tool_calls): messages.append(assistant_msg) # 保留模型这次调用的记录 for tool_call in assistant_msg[tool_calls]: result call_tool( tool_call[function][name], json.loads(tool_call[function][arguments]) ) messages.append({ role: tool, tool_call_id: tool_call[id], content: json.dumps(result, ensure_asciiFalse) }) # 本轮不结束继续把工具结果拿给模型看 continue # 2) 模型直接输出最终回答 final_answer assistant_msg.get(content, ) print(助手, final_answer) messages.append({role: assistant, content: final_answer}) break else: print(助手抱歉执行步骤太多我已经尽力了。)这段代码重点看两个地方第一是循环结构。每轮循环只有两种出口模型返回了工具调用就执行工具、把结果追加到messages然后continue让模型看到新的观察结果模型没返回工具调用说明它认为可以直接回答了就打印内容并break。MAX_STEPS是护栏防止模型陷入无限的“调用工具-再调用工具”死循环。第二是消息顺序。Agent类应用特别讲究消息顺序用户的请求、模型的动作、工具的观察结果都要按时间顺序放进同一个列表。稍微乱序模型就可能“失忆”或者出现幻觉。这也是我见过不少新手翻车的地方。另外temperature我建议调低一些比如0.2到0.4。Agent场景里你要的是稳定、可控、按工具调用逻辑走不是让模型天马行空。温度太高模型可能会把工具名写错、把参数编成不存在的键。我自己第一次跑通这个循环时最开心的不是“能回答问题”而是看到了“模型主动决定调用工具”的那一刻。在终端输入一句“帮我算一下1234*5然后加一条待办‘买硬盘’”模型自动分两次调用工具再给出汇总回答。那个瞬间你会真正理解Agent和聊天机器人的分界在哪里。3.4 记忆从哪来对话上下文管理主循环里所有messages都放在内存中这就是短期记忆。但真实产品只靠内存是不够的换个页面、重启服务历史就没了聊了100轮消息数量也会把上下文窗口撑爆。最小可落地的方法是给会话加一个session_id把messages按session_id存起来比如存进JSON文件import uuid import json class SimpleMemory: def __init__(self, session_idNone): self.session_id session_id or str(uuid.uuid4()) self.file fmemory_{self.session_id}.json def load(self): if os.path.exists(self.file): with open(self.file, r, encodingutf-8) as f: return json.load(f) return [{role: system, content: 你是一个可靠的智能助手可以调用工具完成任务。}] def save(self, messages): with open(self.file, w, encodingutf-8) as f: json.dump(messages, f, ensure_asciiFalse, indent2)这里要提醒一句短期记忆和长期记忆是两件事。短期记忆是消息序列长期记忆则是把历史沉淀成“用户偏好”“关键事实”需要时才检索出来塞进上下文。别一上来就上向量数据库先用简单的JSON把会话续接能力做出来已经能应付大多数学习Demo和中小型业务。等你真的遇到“对话历史太长需要压缩”的问题再考虑两步走老的对话定期做摘要摘要留在上下文里完整内容存到外部存储。4. 真上线才发现的事四个高频坑和排查思路理论说再多不如实战踩一遍坑。下面这几个问题我基本都见过每个都配了排查思路。4.1 模型死活不调用工具还自己编答案现象你明明在tools里定义了函数模型却不调用而是假装自己已经“算好了”或者“记下了”。排查就三步检查工具schema描述。太模糊会导致模型不知道何时使用务必在description里给示例。检查temperature。太高会让模型放飞自我。检查messages结构。有些SDK要求最新的消息是user或tool如果系统消息和工具结果粘在一起模型可能根本没读到工具。我最常用的一招在description里写明“当用户给出一个数学表达式时你必须调用calculator工具不要自己口算”效果立竿见影。4.2 聊了几轮就“失忆”或者上下文被撑爆现象前3轮还很聪明到第10轮就开始忘记用户之前说的内容甚至报“上下文长度超限”。这不是模型变笨了而是给模型的消息太多超出处理窗口或者早期信息被挤出注意力。短期的解决方法是滑动窗口只保留最近N轮完整消息更早的消息转成摘要塞进system。核心代码如下def trim_messages(messages, keep_rounds10): # 保留system随后只保留最近keep_rounds轮 system_part [m for m in messages if m[role] system] history_part [m for m in messages if m[role] ! system] return system_part history_part[-(keep_rounds * 2):]这里用keep_rounds乘2是因为每一轮对话在消息列表里至少包括用户消息和助手消息两条。数字可以按模型窗口大小调节核心原则是别一股脑全塞进去。4.3 响应越来越慢等得心慌现象同样的任务一开始2秒后来8秒甚至更多。Agent慢通常有两个来源一是模型往返次数多二是上下文太长。排查方式打印日志数一下一个任务到底走了几轮。如果每件事都调用5次以上说明工具粒度太碎可以把多个操作合成一个工具。观察messages长度。太长就做裁剪或压缩。检查工具调用是否支持并行。如果模型在一次响应里返回多个tool_calls你可以并发执行而不是逐个串行等。加上超时和重试。大模型API偶尔会有网络抖动超时重试是标配但重试次数不要太多不然延迟雪上加霜。4.4 工具报错了Agent还一副成功的样子这个坑我踩过最多次。现象计算器工具因为表达式非法报错但Agent最后回复用户“好的已经算出来了答案是42”。为什么会这样如果你在call_tool里把异常吞掉了模型根本看不到错误信息它只能根据你回传的“假成功”结果继续编。所以工具层的错误处理第一原则是永远把真实的错误信息以observation的形式交给模型。def call_tool(name, arguments): if name calculator: try: value safe_eval(arguments[expression]) return {status: success, data: value} except Exception as e: return {status: error, error: f表达式解析失败: {e}}模型读到这类结构化错误信息后往往能自主修正表达式或者向用户说明工具的边界。这就是Agent“反思能力”落地的最关键一环。把常见问题汇总成一张表方便收藏问题现象最常见原因一句解决办法不调用工具工具描述模糊、温度过高重写descriptiontemperature降到0.3上下文超限或失忆消息列表无限增长滑动窗口历史摘要响应慢往返次数多、上下文太长合并工具、压缩上下文、并行调用报错却装作成功错误信息没有回传给模型异常信息放进observation5. 从能跑到跑好三个让Agent更靠谱的小优化到这里你已经有了一只能跑的最小Agent。接下来三个优化是我在生产环境里被逼出来的经验每一条都能显著提升可用性。5.1 先确认再动手给Agent加参数校验模型输出JSON并不总是符合schema缺参数、类型错误、甚至拿另一个工具的参数硬塞过来都见过。所以在call_tool入口加一层程序侧的校验而不是无脑信任模型。最简单的写法def validate_args(name, arguments, required_fields): missing [f for f in required_fields if f not in arguments] if missing: return {status: error, error: f缺少参数: {missing}} return None def call_tool(name, arguments): if name calculator: err validate_args(name, arguments, [expression]) if err: return err # 继续执行真正的计算逻辑这样一来就算模型犯错你也能得到一条清晰的错误信息而不是代码抛KeyError导致整个Agent崩掉。5.2 加个“后悔药”重试机制与安全护栏重试听起来简单但有两个细节要仔细想重试谁、重到什么时候为止。Agent场景里重试的不是单个工具而是“让模型重新规划一轮”。也就是说工具失败了把错误observation交给模型让模型自己决定换参数还是换策略你只需要在MAX_STEPS限制范围内给足机会。安全护栏同样重要危险操作删除、发送、支付要有人工确认最好在工具层做“试运行/正式运行”两档整体上先跑只读模式观察模型行为没问题再放开写权限。特别是小白阶段你以为在跟模型聊天实际上模型可能在疯狂调用工具写数据没有护栏出线上事故只是时间问题。5.3 从对话机器人到业务助手最后一点扩展建议这个最小骨架跑通之后想往哪个方向扩展都可以。比较常见的路径接外部API把天气查询、数据库查询、办公软件操作封装成工具扩展手脚。接知识库用向量检索把内部文档塞进上下文让Agent回答私有知识。做多Agent协作一个主Agent负责拆解任务多个子Agent分别处理搜索、计算、审核再把结果汇总。每次扩展前建议做同一件事先改一行代码验证这一层的可行性再继续往上加。我见过太多人第一版就想做“全自动全能助手”最后卡在工程复杂度上连最小闭环都没跑出来。先从“一个工具能正确被调用”开始比什么都重要。文章写到这如果只让你记住一条我希望是Agent不是某个神秘框架带来的魔法而是模型、工具、记忆、循环这四样东西的有机组合。我自己从只会调API到跑通第一个Agent最大的变化不是用了更贵的模型而是真正理解了“让模型带着工具做事”这句话的含义。刚开始你可能觉得这段代码别扭、循环绕没关系多跑几个例子多看看模型在每一步到底做了什么你会慢慢找到把Agent做扎实的感觉。最后再分享一个小技巧每次调试时把messages和tool_calls完整打印出来看模型实际收到了什么、输出了什么比盯着最终回答猜原因高效得多。祝你第一次让Agent调用工具成功时也能感受到那种“居然真的动起来了”的兴奋。