
1. 先别急着装框架到底什么是 AI Agent如果你最近搜过 ai agent大概率会看到两类内容要么是云厂商出的白皮书架构图、名词满天飞要么是短视频标题把 Agent 吹成全自动数字员工。看完之后的真实感受通常是道理都懂但真让自己动手写完全不知道从哪里下手。我也经历过这个阶段。先说结论AI Agent 本质是一套“感知—决策—行动—观察”的循环系统。感知指拿到用户请求和当前状态决策指让大模型判断下一步该做什么行动指调用代码去执行具体动作观察指拿到执行结果后再反馈给模型然后继续决策。这个循环只要在转它就是 Agent如果只调用一次 API 就退出那它只是一个普通聊天程序。这篇文章是我自己从“调包侠”往“造轮子的人”转型的记录。我没有用 LangChain也不依赖 AutoGen甚至连平常用的 agent 脚手架都卸了。目的不是返祖而是想弄清楚框架到底在背后偷偷替我做了多少事。写完之后我才发现Agent 怎么列举可用工具、怎么把模型输出转换成函数调用、怎么在上下文太长时截断这些问题其实绕开框架手写一遍才会真正豁然开朗。这篇文章适合已经会用大模型 API、但还没自己搭过 Agent 的开发者也很适合刚学完提示词工程、想往应用层再迈一步的零基础同学。你不用懂 LangChain也不用懂复杂的部署只要会 Python 基础语法照着我后面的代码走一遍就能收获一个完全属于自己的最小 Agent。1.1 用大白话拆解 Agent 的本质我习惯用一个类比给刚入门的同事讲Agent 就像一个有手机、会查资料、能调用 app 的实习生。你给他一个目标他能自己决定先打开浏览器搜索再调个接口获取数据最后把结论整理好交给你。它不像传统程序那样每个分支都要开发者提前用 if/else 写好。拆开来看Agent 的核心组件只有三个大脑也就是大模型。它负责理解目标、拆解步骤、选择工具、评估结果。大模型不一定每次都准所以关键其实在后两样。手可执行工具。Agent 如果只能输出文字那就是高级聊天机器人只有配上代码执行、API 调用、文件读写这些动作才算真正有“手”。记忆既包括同一轮任务中的消息历史也叫短期记忆也包括跑过的任务、存下的结论也就是长期记忆。没有记忆的 Agent 每轮都在失忆谈不上连续推演。很多框架把这三个组件融合得太顺滑导致出了事故你完全分不清是哪一步引起的。我这次手写之后体会到大脑、手、记忆三者之间的协议就是纯文本消息。你给我一段 JSON我返回一段结果没有任何魔法。1.2 主流架构都在做什么如果去翻 ai agent 主流架构大概率会看到 ReAct、Plan-and-Execute、Reflection 这些词。我用具体场景给它们做个通俗解释。ReAct也就是推理加行动。模型每走一步先思考“我现在缺什么信息”然后调用一个工具去拿信息看到结果后再继续推理。它适合搜索、查数这类需要中途纠偏的任务。实现上就是 while 循环最容易手写。Plan-and-Execute先输出整体计划再逐段执行。适合“帮我生成一份月度总结”这种目标清晰的输出型任务。Reflection执行完以后让模型再检查一遍自己的输出进行修改相当于加了一道自检。它能让最终结果更稳但会多花一次甚至多次模型调用。你不需要一上来就把所有架构都实现一遍。我一向建议先把 ReAct 迭代跑顺因为它最贴底层循环。跑顺了以后回头看Plan-and-Execute 不过是调整了消息的组织方式Reflection 不过是在外层再套一层循环。架构不玄乎区别只是循环的嵌套顺序和次数。1.3 为什么手写一遍比调包一年学得还多市面上“10 分钟搭一个 Agent”的框架教程一抓一大把但如果你能回答下面三个问题才算真的入门了框架怎么决定把哪条用户消息放进工具调用结果模型连续要求调用同一个工具时框架怎么避免死循环多轮对话后历史消息是按什么规则截断的我认识不少准备转 AI 方向的开发者简历里写着“熟练使用 LangChain、AutoGen”被问到 function calling 的本质时就含糊其辞。“调包侠”这个说法最近很流行就是因为大家开始反感只会拼接现成模块、不理解底层逻辑的方式。手写 Agent 不等于排斥框架而是在熟练使用框架之前先赤手空拳走一遍最难的路。这条路走完以后你再回去看任何框架会秒懂它的设计选择哪些地方帮你省了事哪些地方反而是过度封装。该用框架时你照样用但那时你是站在工具之上做判断不是被工具牵着走。2. 动手前先把概念理清Token、上下文与工具调用正式开始写代码前有几个基本功必须先砸实否则后面报错会让你连排查方向都不清楚。2.1 Token 到底是什么为什么它决定 Agent 的“脑子大小”很多刚接触 Agent 的人都会搜“ai agent token是什么意思”。这里我讲个最通俗的版本Token 是大模型处理文本的最小单位。它既不是单词也不是字。中文经常一个字符约等于 1 到 2 个 token英文通常一个单词约等于 1 到 1.5 个 token。模型的本质是“预测下一个 token”所以所有输入输出都得先切分成 token。这个概念对 Agent 特别重要因为大模型的上下文窗口是按 token 数算的。比如某模型支持 128K token你塞进去的内容一旦过长最早的信息就会被挤掉。Agent 是多轮循环每一轮模型回复都会追加到消息列表工具返回结果也在追加token 消耗增长非常快。如果完全不管可能跑到第五轮前面的关键信息就已经被截掉了。实际操作层面我建议估算成本别按字数要按 token。固定模板、示例、工具定义全都要算进去。看日志重点看 prompt_tokens、completion_tokens、total_tokens 三个字段。如果一个任务跑到第 8 步突然变蠢先怀疑上下文被挤爆而不是模型变傻了。2.2 Function Calling 与输出结构化工具调用是 Agent 区别于聊天机器人的设计核心。它有一个容易误解的机制模型本身不执行任何函数它只是在收到工具说明后在回复里输出一个 JSON表示“我想调用 get_weather参数是……”。真正干活的是你自己的代码。我第一次理解偏了以为给 API 传了 tools 参数它就会自动执行。后来才发现 API 只负责描述意图执行全靠外部代码。那些 Agent 框架说白了就是在帮你维护这个来回循环。另一个容易踩的坑是输出格式。模型可能不给你纯 JSON会在前后加说明文字。为了保证 json.loads 能解析我一般会在 system prompt 里要求“只输出合法 JSON”同时在外层用正则提取大括号内容再丢给解析器。你可以把它想象成跟一个聪明的实习生说话你不能只靠对方自觉要提前定好汇报格式和检查机制。2.3 上下文管理一个被大多数人忽略的隐形瓶颈多轮 Agent 跑到后期消息列表会越来越长。把全部历史直接塞给模型会遇到两个问题一是窗口有上限二是成本递增。实践中常用三种方案滑动窗口只保留最近 N 条消息简单直接适合短链路任务。摘要压缩超过阈值后让模型把旧消息压缩成一段 summary用 summary 替代旧内容。向量召回把历史记录分块存入向量库按相关性取回 Top K适合知识密集型长任务。我手写版先用的是“滑动窗口加摘要兜底”。消息超过 20 条时把最早 10 条交给模型换 summary这样既能保留全局大概信息又不会让单次请求膨胀。后面如果场景变复杂再升级到向量存储也不迟。小项目起步阶段别迷信重型组件先把手边的方案跑通更重要。3. 从“调包侠”迈向“造轮子的人”手写最小 Agent这一章我给你一个可以直接运行的最小 Agent。不需要装任何 agent 框架只需要一个 OpenAI 兼容的 API Key 和 Python 环境。依赖库只有 openai 和 json。3.1 不“放着现成不用、偏要硬造”的合理理由可能有人会说网上现成框架那么多为什么要自己写我回答就是因为学做菜不能一直买预制菜包。你如果只知道加热永远不知道盐什么时候放等你会自己炒一遍再看预制菜包的配料表心里就非常有数。手写版本一定丑、功能少、边界处理粗糙但这恰恰是价值所在。你会在反复修补中理解 Agent 每个零件怎么协同。以后再回头看 LangChain你会发现它内部也还是同一套逻辑维护 messages、循环调用模型、解析 tool_calls、执行工具、回填结果。3.2 用 Python 写出 Agent 核心循环下面这段代码是典型的 ReAct 循环。我刻意去掉了 UI 和复杂异常处理保留最核心骨架。import json from openai import OpenAI client OpenAI() TOOLS [ { type: function, function: { name: get_weather, description: 查询指定城市的天气。, parameters: { type: object, properties: { city: {type: string, description: 城市名} }, required: [city] } } } ] def call_llm(messages, toolsNone): resp client.chat.completions.create( modelgpt-4o-mini, messagesmessages, toolstools, ) return resp.choices[0].message def execute_tool(name, arguments): # 这里是你的“手” if name get_weather: city arguments.get(city) return f{city} 今天多云气温 18℃ return unknown tool def run_agent(user_input, max_steps10): messages [{role: user, content: user_input}] for step in range(max_steps): message call_llm(messages, TOOLS) # 模型没有要求调用工具说明任务完成了 if not message.tool_calls: return message.content # 把模型的调用意图追加进消息历史 messages.append(message.model_dump()) for tool_call in message.tool_calls: fn_name tool_call.function.name fn_args json.loads(tool_call.function.arguments) result execute_tool(fn_name, fn_args) messages.append({ role: tool, tool_call_id: tool_call.id, content: str(result) }) return 超出最大步数已停止 print(run_agent(帮我查一下北京的天气))这段代码的核心思想是把用户消息丢给模型同时告诉模型有哪些工具可用。如果模型返回普通文本说明不再需要额外信息任务结束。如果模型返回 tool_calls就说明它还想调用工具。我们在自己代码里执行工具然后把结果以 roletool 的形式回传。循环继续直到模型不再请求工具或超过步数上限。我第一次手写时踩过最大的坑是忘了把模型的 tool_calls 消息追加回历史。不追加的话模型下一轮会“失忆”根本不知道它刚想调什么工具会重复生成同样调用形成死循环。这一步就是 Agent 的短期记忆线千万不能省。3.3 再进一步做一个工具注册表真实项目里 Agent 要用的工具不止一个用 if/else 堆会很痛苦。我建议用字典维护一张工具注册表把函数直接挂上去然后自动生成给模型看的工具描述。import inspect TOOL_FUNCS {} TOOLS [] def register(func): TOOL_FUNCS[func.__name__] func schema { type: function, function: { name: func.__name__, description: inspect.getdoc(func) or , parameters: { type: object, properties: {}, }, } } # 实际项目中可以根据函数签名自动解析参数类型 TOOLS.append(schema) return func register def get_weather(city: str): 查询指定城市的天气。 return f{city} 今天多云气温 18℃ def execute_tool(name, arguments): if name not in TOOL_FUNCS: return error: tool not found return TOOL_FUNCS[name](**arguments)这个设计的好处是以后要加工具我只写一个普通 Python 函数换行加装饰器即可。模型看到 tools 后会用 JSON 来指定调用谁、传什么参数我的 execute_tool 再负责分发。框架里的注册机制本质也是这个思路。3.4 加记忆从“一轮失忆”到“多轮对话”只有循环和工具Agent 还不太够它需要对多轮对话保持记忆。最简单的方式是把 history 作为参数传进来每次执行完更新。def run_agent_with_history(user_input, historyNone, max_steps10): if history is None: history [] messages history [{role: user, content: user_input}] # 循环逻辑同上 return result, messages但前面说过历史信息不能无限膨胀。我的实际做法是维护一个消息列表超过 20 条就触发摘要压缩。把最老的 10 条丢给模型让它生成一段 summary再把这个 summary 作为 system 消息重新放进 messages。代价只有一次模型调用却能解决 90% 的上下文问题。4. 让 Agent 真正跑起来真实场景与部署细节代码能跑通以后大家最关心的就是“放到哪跑”和“能不能真用起来”。这里我讲两个方向个人自动化场景以及服务器部署的注意点。4.1 真实场景让 Agent 处理重复性事务的边界最近总能刷到“ai agent 让小红书自动发消息”这类词。先给想照着做的朋友泼盆冷水跨应用自动化不是不能做但必须先把边界想清楚。批量采集、私信轰炸、大量自动评论这类行为既违反平台规则也容易造成账号风险。你做技术分享不应该教大家把这些能力用在灰产方向上。真正适合 Agent 的自动化场景反而很朴素。我自己常用的有每天早上抓取订阅的行业新闻生成摘要发给自己的邮箱。定时把团队周报汇总成 markdown 表格。读取收藏夹里的文章自动提取要点和标签分类归档。这些任务有两个共同点重复度高、耗时长、判定标准相对固定。Agent 跑起来以后等于帮我把“人肉搬运”的活儿接了过去而且出错时只需要看日志就能复盘。如果你未来接到“让 Agent 去操作某个 App”的需求记住一条原则授权范围要小权限要可撤回每一步动作要能被日志追踪。不要上来就把所有接口全开放给模型不然一个错误参数就能让程序连续执行几十个不该发生的动作。4.2 部署到服务器要注意的细节本地跑得顺放到服务器就翻车这是 Agent 部署最常见的现象。我整理了自己踩过的几个坑API Key 没有用环境变量管理要么丢失要么直接暴露在代码仓库。服务器网络出口有限制客户端根本访问不到模型 API导致超时。服务器上的 openai 库版本与本机不一致参数兼容性出问题。任务没有超时控制一个工具调用挂死整个进程都卡住。实用做法每个工具执行都套超时每一步运行日志都落盘文件名带上任务 ID 和时间戳API Key 统一放在 .env 并确保它不会提交到公共仓库。我之前看过阿里云发布的 agent 白皮书类资料里面反复强调的就是计费、并发、监控、灾备这些工程问题。这也印证了一点Agent 上生产环境之后难点不在 prompt而在工程基建。小项目部署我建议用这个最小方案组件推荐方案说明主进程Python FastAPI 或 CLI迭代快方便调试任务调度cron 或简单队列定时触发或串行执行日志存储每日一个 JSONL 文件方便回放和排错记忆存储SQLite 起步单文件个人项目足够模型接入官方 API 或兼容接口优先选稳定且易切换的方案4.3 Rust 也能做 Agent但别着急热搜里还有“基于 rust 语言 ai agent”的说法。我了解了一下现状Rust 在 Agent 生态里确实有人在做有人用 Rust 调用 HTTP API 搭一个基础循环有人把执行频率最高的工具模块用 Rust 重写还有人把 Rust 编译成 WASM 供 Python 调用来降低延迟。但我劝还没上手的朋友别现在就直接跳 Rust。Agent 开发里有大量时间花在调试 prompt、工具参数、返回格式上Python 本身的迭代速度更有优势。等你真的跑到了产品级遇到并发瓶颈或者需要在资源受限设备上部署再考虑把热点路径迁移到 Rust 也不迟。选型是需求逼出来的不是热搜词带领的。5. 实战中踩过的坑与排查技巧这一章是我想分享的最核心经验。因为实现 Agent 的代码框架很容易找但里面藏着的坑文档几乎不会写。5.1 模型一本正经地胡说八道怎么办Agent 拿到工具结果后依然可能在下一步推理里得出错误结论。比如我让模型调用计算工具算 8 除以 32工具明明返回了 0.25模型最终回答却写成了 25。原因很简单工具结果和模型推理是两条信息流模型可能没把数值读进去。我现在的处理方式是工具返回值尽量组织成一句话直接带出最终答案减少二次推断。数值类任务加一个程序侧校验不依赖模型自查。关键任务让模型输出结构化推理 JSON程序再做规则校验通过后才执行。5.2 被 Token 计费吓到控制预算的土办法调包阶段几乎没人看 token手写之后才第一次直观感受到每一轮循环都在花钱。我的三个土办法每轮打印 prompt_tokens 和 completion_tokens超过阈值立刻截断。工具返回内容做精简不要把远端 API 的原始响应全量丢回模型。高频、固定数据加缓存比如天气结果十分钟内基本不变直接复用不再额外调模型。原则只有一句话模型只需要看到解决问题所必需的信息多出来的 token 全是成本和干扰。5.3 工具调用失败与异常边界工具执行失败是常态因为模型生成参数本来就带不确定性。常见失败类型有参数缺字段、类型不匹配、远端 API 超时、返回结果超长。我踩过最疼的一次是模型反复调用一个已“权限不足”的 API。原因是每次失败后我都把原始错误原样返回给模型。模型误以为“换个参数就能成功”于是换着花样试了几十次直接把预算打光。之后我在工具返回里加了硬规则遇到权限错误就返回“请停止调用此工具并告知用户权限不足”同时要求模型不要重试。给模型写禁止指令和给人写权限规范一样重要。5.4 调试 Agent 的非常规方法普通程序调试靠断点Agent 调试靠“看它在每一轮怎么思考”。我的习惯是给每个循环步骤加 step_id然后输出一行可读日志step3 model_message: want to use tool get_weather tool_result: {city:北京} - 多云 18℃这种日志存成 JSONL出问题时直接按最新日志往前回溯能快速定位是推理错了、工具错了还是上下文被截断了。测试阶段我会把模型温度调到 0减少随机性max_steps 设成 5 以内让 Agent 快速失败而不是跑几分钟才暴露问题。链路稳了以后再逐步放开温度、步数和工具数量。我在实际写 Agent 项目时最深的一个体会是Agent 并不是玄学它就是一个被大模型驱动的状态循环。最难的从来不是代码怎么写而是怎么让每一步的“决策信息流”保持干净、可控、可追踪。把工具返回规范好、把模型看到的信息压缩到最精简、把日志记录清楚这三件事的价值远大于调一个花哨的 prompt。如果你也想从“调包侠”慢慢变成“造轮子的人”我的建议很简单别急着套框架先照着上面的最小闭环写一个自己的 Agent。哪怕第一版只能查个天气你也会真正理解轮子是怎么转起来的。后面不管是加记忆、加工具还是用 Rust 重写关键模块都会顺畅很多。