
别被“AI Agent”这个词吓到。我当年第一次听到这个概念脑海里浮现的是科幻片里那种能独立执行任务的机器人总觉得要会分布式系统、要懂一堆工程框架才有资格碰这个东西。后来被一个朋友拉着从零搭了一个小智能体我才发现Agent 的入门门槛没有想象中那么高但它背后的设计思路——怎么让模型自己决定调用什么工具、怎么把结果喂回去继续推理——确实需要一点耐心去理解。这篇文章不会教你“用某个框架三分钟生成一个 Agent”因为那种速成作品解决不了任何实际问题。我会从原理到代码带你亲手写一个真正能用的智能体它能理解你的问题自己判断是否需要调用工具拿到工具结果之后再组织语言回答你。我用的语言是 Python整体代码量不大每段关键代码我都会解释为什么这么写。如果你是第一次接触智能体开发哪怕是只会最基础的 Python 语法照着抄一遍也能跑通。1. Agent 没那么玄乎先建立正确的心智模型很多人学 Agent 一开始就陷进框架文档里结果越看越懵。我建议反过来先别写代码先把 Agent 到底是什么这件事想清楚。1.1 普通聊天机器人和智能体的分水岭在哪儿普通聊天机器人就是你问一句它答一句。它像一个知识渊博但没有手和脚的顾问只能凭训练时见过的数据回答你。你在群里问“明天适合爬山吗”它能给你一段车轱辘话但既查不了天气预报也算不了空气质量。Agent 的差别在于它把“说话”和“做事”打通了。同样是“明天适合爬山吗”这个问题一个合格的 Agent 会先拆解任务爬山需要知道天气、气温、风力甚至空气质量。然后它会去调用天气查询工具拿到结构化数据再结合这些数据给你一个明确的结论。普通机器人是“我知道什么就告诉你什么”Agent 是“我不知道就去查查完再告诉你”。这个分水岭听起来简单却是整个智能体开发的灵魂。1.2 拆开看Agent 就是一个“思考-行动-观察”的循环我搭完第一个 Agent 之后最大的感悟是它本质上是在跑一个循环。第一步模型拿到你的问题进行推理——“这个问题我需要查天气”。 第二步模型输出一个结构化的工具调用指令而不是自然语言。 第三步你的代码执行这个指令比如真的请求天气接口。 第四步把工具返回的结果当成一条新消息再次交给模型。模型看到工具结果后如果信息够了就给你最终答案如果不够它就再次输出下一个工具调用。这个循环不断重复直到模型认为问题已经解决。你在各种演示里看到的“智能体自动完成多步骤任务”拆到底都是这个循环推理、调用工具、看到结果、再推理。模型是大脑工具是手脚中间那层胶水代码就是我们写的 Agent 本体。想明白这一点之后后面所有代码都是在实现这个循环而已。2. 选型别掉进“框架越多越好”的陷阱在动手之前有两类选型问题躲不开用什么模型用什么框架。我先说结论——对于新手选型的优先级是模型能稳定支持工具调用 接口简单 框架花哨程度。2.1 模型选型核心是接口能力不是参数大小很多新手选模型第一反应是“哪个聪明选哪个”。但搭 Agent 这件事模型聪明不聪明只是其中一个维度更重要的是它对外提供的接口能力。我在实际项目中主要看三个标准考察维度为什么重要入门建议是否支持函数调用没有这个能力模型就无法输出结构化工具指令Agent 循环就转不起来优先选明确支持工具调用协议的服务上下文窗口长度每次循环都会把历史消息重新发给模型窗口太小容易爆8K 起步32K 以上会更从容返回速度与稳定性循环意味着一次任务有多次模型请求单个请求慢整体体验会翻倍慢优先选响应快、限流少的方案参数大小反而没有那么关键。我的经验是一个 70 亿参数的小模型只要函数调用格式稳定也能把一个单工具任务跑得很好反过来一个大模型如果工具调用协议不成熟你会在这上面折腾到崩溃。2.2 为什么我建议先自己手写一遍再上框架市面上的 Agent 框架已经很成熟了封装了记忆、工具注册、多智能体编排一大堆能力。那为什么我还建议你从零手写一个因为框架会掩盖细节。我第一次用框架的时候给了模型一个工具清单它总是调不对参数我根本不知道问题出在我给的工具描述不清楚还是框架的解析层有问题。后来我自己写了一个最朴素的循环才把整条链路看明白原来模型要的不是“接口名 参数”而是一份符合特定格式的工具说明文档。自己手写一遍你会理解三件事工具描述是怎么影响模型决策的工具返回结果是在哪一步塞回上下文的循环失控时最容易卡在哪个环节。有这层理解之后再回去用框架你会发现自己能看懂它的日志了出了问题也知道往哪查。这就是我给所有新手的建议先手写一个玩具版再考虑框架效率。2.3 环境准备其实很简单不用搭什么复杂环境。一个 Python 3.10一个requests库就够了。模型方面你可以用任意提供 Chat 补全接口且支持函数调用的服务我建议先注册一个带免费额度的服务商把 Key 配到环境变量里export LLM_API_KEY你申请的密钥 export LLM_API_BASEhttps://你的模型服务地址/v1不需要一开始就本地部署模型那会把你的学习成本拉高好几倍。先用现成的接口把逻辑跑通这是最快路径。3. 第一版 Demo几十行代码把一个会“用工具”的智能体跑起来这一章是全文的核心。我会给你一份完整的 Python 代码它实现了一个最简单的 Agent能查天气、能看当前时间并根据这两个信息回答你的问题。3.1 核心交互协议让模型调用你的函数在写代码之前你需要理解“函数调用”这个交互协议。它分为两步第一步你把工具的描述以 JSON 格式发给模型告诉它有哪些函数、每个函数接收什么参数、参数是什么类型。模型读完这份“说明书”才知道自己可以调什么。第二步模型在回答过程中如果觉得需要查信息它不会直接执行代码而是输出一个特殊的结构化消息里面包含函数名和参数。真正调用函数的是你自己的程序。理解这个协议之后你会明白模型的角色永远是“决策者”执行者是你。这避免了让模型直接接触外部环境安全边界清晰了很多。3.2 完整代码我的第一个能查天气的智能体下面这段代码是我自己写的第一个 Agent 的简化版。它非常朴素但五脏俱全import os import json import requests from datetime import datetime API_KEY os.getenv(LLM_API_KEY) API_BASE os.getenv(LLM_API_BASE) MODEL os.getenv(LLM_MODEL, default-chat-model) SYSTEM_PROMPT ( 你是一个智能助手。你有调用工具的权力。 当你需要外部信息时请调用对应工具 拿到工具结果后再结合结果回答用户。 ) TOOLS [ { type: function, function: { name: get_weather, description: 查询指定经纬度位置的当前天气情况, parameters: { type: object, properties: { latitude: {type: number, description: 纬度例如 39.9}, longitude: {type: number, description: 经度例如 116.4} }, required: [latitude, longitude] } } }, { type: function, function: { name: get_current_time, description: 获取当前日期和时间, parameters: {type: object, properties: {}} } } ] def call_tool(name, args): if name get_current_time: return {current_time: datetime.now().isoformat()} if name get_weather: # 这里替换成你自己的天气服务请求即可 return { latitude: args[latitude], longitude: args[longitude], weather: 晴, temperature_celsius: 26, wind_level: 3 } raise ValueError(f未知工具: {name}) def chat_once(messages): payload { model: MODEL, messages: messages, tools: TOOLS, tool_choice: auto, } resp requests.post( f{API_BASE}/chat/completions, headers{Authorization: fBearer {API_KEY}}, jsonpayload, timeout60, ) resp.raise_for_status() return resp.json()[choices][0][message] def run_agent(user_input, max_steps8): messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_input}, ] for step in range(max_steps): msg chat_once(messages) messages.append(msg) if msg.get(tool_calls): for tc in msg[tool_calls]: fn tc[function] name fn[name] args json.loads(fn[arguments]) result call_tool(name, args) messages.append({ role: tool, tool_call_id: tc[id], content: json.dumps(result, ensure_asciiFalse), }) continue return msg[content] return 我在限定步数内没有完成这个任务请稍后再试。 if __name__ __main__: print(run_agent(现在几点了顺便帮我查一下北纬 39.9、东经 116.4 位置的天气我该穿短袖吗))这段代码的核心逻辑就是你前文看到的那个“思考-行动-观察”循环。模型先输出工具调用请求程序执行工具并回传结果模型再次生成回答直到不再产生新的调用为止。3.3 代码关键点拆解为什么这样写我在写这个 Demo 时反复调整过几个地方这里只挑最关键的讲。第一点是messages.append(msg)这一行。很多新手会漏掉这一步把模型生成的那条带工具调用的消息放进历史。如果你不把它加回去模型不知道“它刚刚已经发起了调用”紧接着你又把工具结果塞进来模型会陷入错乱。第二点是工具返回结果的格式。role必须是tool并且要带上tool_call_id这样模型才能把工具结果和它刚才发起的调用对应上。这个对应关系一旦错乱模型就会出现答非所问。第三点是max_steps限制。我在循环外面套了一个最大步数防止模型陷入无限调用。这一步看着不起眼真实场景里能救命。第四点是tool_choice: auto。这个参数告诉模型你可以自行决定要不要调用工具。有些场景你可能希望强制它先调某个工具那可以把auto改成具体工具名但在这里让模型自己判断才是 Agent 的意义。3.4 跑起来你会看到这样的过程运行这段代码你可能以为它会直接告诉你答案。但真正发生的过程在日志里会暴露得很清楚第一步用户提问。第二步模型返回一条tool_calls内容是“同时调用 get_current_time 和 get_weather参数是经纬度”。第三步程序执行这两个函数把时间、天气数据拼成一条结果塞回消息列表。第四步模型看到结果生成最终回答“现在是 14:30天气晴26 度微风可以穿短袖。”我第一次跑通这个循环的时候心情很复杂模型确实在“自己决定调用工具”但这个过程中真正干活的是我那几十行代码。Agent 不是什么魔法它就是一套精心设计的循环逻辑。4. 从“能跑”到“能用”记忆、工具边界和多轮状态很多人的 Agent 停在 Demo 阶段就是因为只实现了“能跑”没处理“能用”。这一章我把升级过程中必须面对的三个问题讲透。4.1 短期记忆消息列表本身就是记忆Agent 的记忆问题被很多人讲得很玄。其实最基本的记忆机制就藏在消息列表里你把每一轮的系统提示、用户消息、模型回复、工具结果全部累积在一个数组里每次请求时都带上模型就“记得”前面发生了什么事。这是最朴素的短期记忆。它的优点是无状态、好调试坏处是 token 消耗会随对话长度不断增长。你聊了二十轮之后光是把历史消息重新发送一遍就要花不少时间。针对这个小问题我的折中方案是给消息列表设置一个滑动窗口只保留最近五六轮的内容。再老的消息可以用一句话概括成摘要塞进系统提示词里。这样既保留对话上下文又控制 token 增长。我试过直接截断历史消息效果很差模型会突然“失忆”。后来改成摘要虽然摘要会损失一些细节但整体可用性强太多了。4.2 工具注册表别让模型瞎调用Demo 里只有两个工具问题不明显。一旦工具变多你需要一套“工具注册表”来管理它们而不是把每个工具的逻辑都硬编码在call_tool里。我习惯的做法是这样每个工具都是一个函数函数自带名称和描述程序启动时遍历所有函数自动生成 TOOLS 列表发给模型调用时再根据函数名去注册表里找到对应的函数执行。TOOL_REGISTRY {} def register_tool(func): TOOL_REGISTRY[func.__name__] func return func register_tool def get_weather(latitude: float, longitude: float): 查询指定经纬度位置的当前天气 return {weather: 晴, temperature_celsius: 26}这种设计的两个好处新增工具成本极低只要写一个普通函数再挂个装饰器调用层面不会出现“模型返回了一个不存在的工具名程序直接崩溃”的问题可以做一个统一兜底。4.3 多轮对话状态加一个会话管理外壳如果不做会话管理你的 Agent 只能处理单次提问每次启动都是一张白纸。真实使用场景里用户会说“刚才那个城市呢”这种话所以你得给 Agent 加上会话外壳。最简单的实现是维护一个会话 ID 到消息列表的映射。用户发起请求时带上会话 ID程序把历史消息取出来追加新问题调用循环再把最新的消息列表存回去。sessions {} def chat_in_session(session_id, user_input): if session_id not in sessions: sessions[session_id] [ {role: system, content: SYSTEM_PROMPT} ] sessions[session_id].append({role: user, content: user_input}) result run_agent_with_messages(sessions[session_id]) sessions[session_id].append({role: assistant, content: result}) return result这个外壳看着简单却是你从写脚本到写应用的重要一步。它意味着你的 Agent 可以嵌入到聊天软件、网页应用甚至命令行工具里变成真正可以持续交互的服务。5. 踩坑实录搭智能体最容易翻车的五个地方我前前后后调过不少 Agent 项目有些坑反复踩。这一章不说正确做法只讲翻车案例每个都能对应到一段真实调试经历。5.1 没有最大步数限制模型陷入工具调用死循环首次跑通带多工具的 Agent 时我遇到过模型反复调用同一个工具的场景查完天气再查天气第三次还是查天气每次参数还都一样。原因很简单模型生成的答案因为某种原因不符合它自己的预期它就不断尝试重复调用试图“修正”一个不存在的问题。这种问题不加max_steps限制的话你的账单会先崩溃。我现在的习惯是所有 Agent 循环默认限制三步日常用六步最多不超过十步。超出步数直接停止把已收集到的信息返回给用户并诚实说“我还没完成”。5.2 模型返回的工具参数不是合法 JSON函数调用协议要求模型输出 JSON 格式的参数但实际运行中你拿到的arguments字段偶尔会是残缺的字符串比如少了一个右括号或者参数名被加了多余的空格。我最初看到这个问题很崩溃甚至怀疑是模型服务的问题。后来排查发现根源在于我写的工具描述不够精细参数名、类型、是否必填都没有说清楚模型只能“猜”。解决办法也很简单把工具描述写得像产品说明书一样清楚加上合理的required列表并且每调用模型前提醒它“严格按 JSON 格式输出参数”。另外在代码里一定要做一层容错arguments解析失败时把这个错误信息返回给模型让它重新生成而不是直接让程序崩溃。5.3 上下文爆炸长会话把模型“喂”慢第一次把 Agent 接入长对话场景时我发现问答越来越慢最后甚至直接超时。打开日志一看消息列表里堆了几十条历史再加上工具返回结果每次都发了十多万 token 给模型。这个问题的根源还是我在前面说的滑动窗口没做。我的处理方案是设定一个阈值比如消息超过二十条就触发历史压缩把最早的十轮消息用一个本地模型生成的摘要替换掉。这个方法的效果立竿见影响应速度恢复到了正常水平。5.4 工具执行异常没有回传给模型有一次工具请求超时了我的代码抛出了异常结果整个 Agent 直接崩溃。事后我想明白了一个道理Agent 的外部依赖不可能永远可靠工具失败是常态关键是失败之后怎么处理。正确做法是把异常信息转成文本作为工具结果返回给模型让模型知道“刚才那个调用失败了请换个思路”。比如try: result call_tool(name, args) except Exception as e: result {error: f工具调用失败: {e}}模型收到这个错误说明之后通常会换个方式继续尝试或者直接在答案里告诉用户“天气服务暂时不可用”。这比让整个程序崩溃体面得多。5.5 重试与幂等别让同一个工具被反复执行最后这个坑比较隐蔽。当 Agent 循环重发请求时如果消息列表里包含的历史工具调用记录被重复发送模型可能误以为应该再次执行同样的调用导致同一个副作用被执行多次。比如“发送邮件”这种工具执行两次你就尴尬了。我的策略是对工具的调用记录做全局幂等控制。每条工具调用记录都有一个唯一 ID程序在执行前先检查这个 ID 是否已经执行过执行过就直接返回上次结果不再重复运行。这个策略初看不重要但一旦你的 Agent 从“查天气”升级到“下单、发消息、写文件”它就是必须做的事。6. 从玩具到工具后续值得做的几个进阶方向如果你已经照着前面的内容搭出了一个能跑通的小智能体恭喜你最难的部分已经过去了。接下来怎么继续我根据自己的经验给几个方向每个都是真实使用中会碰到的痛点。6.1 让 Agent 自己找知识接入检索能力基础 Agent 的软肋是知识更新不及时。模型训练完之后新知识它一概不知道。解决这个问题的方法是检索增强把外部文档切块、向量化、存进向量库Agent 需要信息时先做一次向量检索把命中的片段塞进上下文再让模型回答。这相当于给 Agent 装了一个“图书馆查询员”。实现思路不复杂但效果提升非常明显尤其是你做垂直领域应用——比如客服、私有知识库问答——几乎是必备能力。6.2 多 Agent 协作拆成“规划者 执行者”单 Agent 什么都干上下文会越来越乱。我现在比较喜欢拆成多个小 Agent 分工合作一个负责理解任务、拆解计划另外几个分别负责检索、计算、写报告。布局听上去复杂实际上就是把不同功能的 Agent 串起来上游 Agent 的输出作为下游 Agent 的输入。如果你只有一个模型接口也可以不发多个 Agent而是让同一个模型扮演不同角色在不同阶段切换系统提示词。效果有差距但思路是通的。6.3 定时触发与持久化让 Agent 从“应答”到“主动干活”聊天机器人的交互模式是“用户发起Agent 响应”。真要把它变成生产力工具还需要让它定时主动跑起来。我之前给一个内部工具加了一个调度器每天上午九点自动拉取数据、生成摘要、发到群里。模型在里面的角色不变只是交互方向变了。实现方式没有任何魔法就是cron或任务队列 Agent 状态持久化。把会话状态存到数据库里Agent 就从一个随用随丢的脚本变成了可持续运行的服务。6.4 本地化部署隐私与成本的终极解法最后说一句本地部署。很多人一看到“本地部署”就想到要买吓人的显卡其实现在很多模型压缩技术已经让中等配置的机器也能跑得动小参数模型。如果你的场景对延迟不敏感、数据又不能出内网本地小模型完全可行。不过我的真实体会是不要一上来就本地部署。先用云接口把业务逻辑验证清楚确认值得投入了再考虑本地化。否则你会被环境配置、推理速度、模型效果这一圈问题劝退捡了芝麻丢了西瓜。我做了这么多 Agent 相关的小项目之后最深的一个体会是Agent 的边界在于“工具”和“任务目标”不在于“模型多聪明”。你在前面给它多少可靠的工具、设计多清晰的循环边界它就能帮你办多少事。整理这份教程的基础不是我一开始就懂这些而是踩过一连串坑之后一点点把这段朴素经验沉淀出来的。希望你跑通第一版 Demo 的时候也能感受到那种“原来它真的是在想事情”的兴奋。