
1. 为什么我不建议你从框架开始学 AI Agent1.1 一个让我走了半年弯路的认知陷阱刚接触 AI Agent 那会儿我跟大多数人一样上来就找框架。LangChain、AutoGPT、MetaGPT哪个火就装哪个跑通一个 Demo 就觉得自己“会了”。结果呢线上稍微换个模型、改个 Prompt 结构整个链路就崩排查半天连问题出在哪一层都定位不到。后来我才想明白一件事框架帮你屏蔽了细节但恰恰是这些细节决定了 Agent 到底能不能扛住真实场景。这个项目标题里有个词特别戳我——“手写”。不是“基于某某框架搭建”而是“手写”。这意味着我们要从最底层开始把 Agent 的每一个零件都自己拧一遍。为什么值得这么干因为当你亲手写过一遍 ReAct 循环、亲手实现过工具调用的解析逻辑、亲手处理过上下文超限的截断策略之后再回头看那些框架你会发现它们不过是把你写过的东西封装得更漂亮而已。到那个时候你用框架是“选择”而不是“依赖”。这篇文章适合谁如果你已经会用 Python 写点脚本调过 OpenAI 或者千问的 API但总觉得 Agent 这东西“知其然不知其所以然”那这篇就是写给你的。如果你完全零基础也没关系我会把每个关键概念用生活化的方式讲清楚代码部分尽量给全你可以直接抄作业。1.2 手写 Agent 到底在写什么很多人以为 Agent 就是“让大模型调用工具”这话对了一半。完整的 Agent 至少包含四个核心模块推理循环Reasoning Loop、工具注册与调用Tool Registry Invocation、上下文管理Context Management、结果解析与回传Output Parsing。框架帮你做的无非是把这四个模块用一套抽象接口串起来。手写的意义在于你会被迫面对每一个脏活累活。比如模型返回的 JSON 格式不对怎么办工具调用超时了怎么重试多轮对话之后上下文爆了怎么截断这些问题在框架里可能就是一个参数但手写的时候你得自己想清楚策略。而正是这些策略决定了你的 Agent 在生产环境里是“能用”还是“好用”。我个人的经验是手写一遍之后你对 Agent 的理解会从“调 API”跃升到“设计系统”。这个跃升才是从“会调用”到“懂原理”的真正分水岭。2. 动手之前先把这几个核心概念钉死2.1 Agent 和普通 Chatbot 的本质区别普通 Chatbot 的逻辑是用户输入 → 模型生成 → 返回结果。一条直线没有分支。Agent 的逻辑是用户输入 → 模型思考 → 决定是否调用工具 → 执行工具 → 把结果喂回模型 → 模型再思考 → 循环直到得出最终答案。这是一个带反馈的闭环。这个闭环里最关键的是“决定是否调用工具”这一步。模型怎么知道该调用哪个工具靠的是我们在 System Prompt 里写的工具描述。所以工具描述写得好不好直接决定了 Agent 的智商。我见过太多人工具描述就写一句“查询天气”结果模型根本不知道什么时候该用它。正确的写法应该是“当用户询问某个城市的天气、温度、是否下雨等信息时调用此工具。输入参数为城市名称输出为当前天气状况。”2.2 ReAct 模式手写 Agent 的骨架ReActReasoning Acting是目前最主流的 Agent 推理模式。它的核心思想是让模型在每一步都输出“思考”和“行动”两部分。思考部分是模型对当前状态的分析行动部分是模型决定下一步做什么——要么调用某个工具要么给出最终答案。手写 ReAct 循环的伪代码大概长这样while not done: response model.generate(context) thought, action parse(response) if action.type tool_call: result execute_tool(action.name, action.args) context.append(result) elif action.type final_answer: done True return action.content看起来简单但魔鬼在细节里。parse函数怎么写模型不按格式输出怎么办工具执行报错了怎么处理这些才是手写 Agent 的真正难点。2.3 工具调用的三种实现路径目前让模型调用工具主流有三种方式方式原理优点缺点Prompt 解析在 Prompt 里约定输出格式自己解析不依赖模型能力通用性强格式不稳定解析容易出错Function Calling模型原生支持返回结构化调用信息稳定、准确需要模型支持部分模型不兼容混合模式优先用 Function Calling失败降级到 Prompt 解析兼顾稳定性和通用性实现复杂度高我个人的建议是如果你用的模型支持 Function Calling比如千问、GPT-4 系列优先用原生方式。如果不支持或者你想深入理解原理那就从 Prompt 解析开始手写。这个项目里我会两种都讲你可以根据自己的模型情况选择。3. 从零搭建手写 Agent 的完整实操3.1 环境准备与项目结构先把环境搭起来。Python 3.10 以上装几个必要的库pip install openai requests pydantic项目结构我建议这样组织my_agent/ ├── agent.py # 核心推理循环 ├── tools.py # 工具注册与实现 ├── parser.py # 输出解析 ├── context.py # 上下文管理 └── main.py # 入口为什么要把 parser 和 context 单独拆出来因为这两个模块是最容易出问题的地方拆出来之后方便单独测试和替换。我试过把所有逻辑塞在一个文件里后期改起来简直是灾难。3.2 工具注册让模型知道它能干什么工具注册的核心是两件事定义工具的执行逻辑以及生成给模型看的工具描述。先看代码import json class Tool: def __init__(self, name, description, parameters, func): self.name name self.description description self.parameters parameters self.func func def to_schema(self): return { name: self.name, description: self.description, parameters: self.parameters } def execute(self, **kwargs): return self.func(**kwargs) TOOL_REGISTRY {} def register_tool(name, description, parameters): def decorator(func): TOOL_REGISTRY[name] Tool(name, description, parameters, func) return func return decorator register_tool( nameget_weather, description当用户询问某个城市的天气状况时调用此工具, parameters{ type: object, properties: { city: {type: string, description: 城市名称} }, required: [city] } ) def get_weather(city): # 实际项目中这里调用天气 API return f{city}今天晴气温 25 度这里有个关键点description的写法。我踩过的坑是描述写得太简略模型根本不知道什么时候该调用。后来我总结了一个模板“当用户询问 XXX 时调用此工具输入为 XXX输出为 XXX”。把触发条件、输入、输出都说清楚模型的调用准确率会明显提升。3.3 核心推理循环Agent 的心脏这是整个项目最核心的部分。我用的是 ReAct 模式让模型每一步都输出思考和行动import json from openai import OpenAI client OpenAI(api_keyyour-key, base_urlyour-base-url) SYSTEM_PROMPT 你是一个智能助手可以使用工具来帮助用户解决问题。 你可以使用以下工具 {tool_descriptions} 请严格按照以下格式回复 思考你对当前问题的分析 行动你要执行的操作格式为 JSON {{type: tool_call, name: 工具名, args: {{...}}}} 或 {{type: final_answer, content: 最终答案}} def build_system_prompt(): tools_desc \n.join([ f- {t.name}: {t.description} for t in TOOL_REGISTRY.values() ]) return SYSTEM_PROMPT.format(tool_descriptionstools_desc) def parse_response(text): # 提取行动部分的 JSON action_start text.find({type) if action_start -1: return None, text action_str text[action_start:] try: action json.loads(action_str) thought text[:action_start].strip() return thought, action except json.JSONDecodeError: return None, text def run_agent(user_input, max_turns10): messages [ {role: system, content: build_system_prompt()}, {role: user, content: user_input} ] for turn in range(max_turns): response client.chat.completions.create( modelqwen-plus, messagesmessages, temperature0 ) reply response.choices[0].message.content thought, action parse_response(reply) if action is None: return reply if action[type] final_answer: return action[content] if action[type] tool_call: tool_name action[name] tool_args action.get(args, {}) if tool_name not in TOOL_REGISTRY: result f错误工具 {tool_name} 不存在 else: try: result TOOL_REGISTRY[tool_name].execute(**tool_args) except Exception as e: result f工具执行错误{str(e)} messages.append({role: assistant, content: reply}) messages.append({role: user, content: f工具执行结果{result}}) return 达到最大轮次限制未能完成任务这段代码有几个设计决策值得说明。第一temperature 设为 0。Agent 场景下我们需要的是稳定和可预测不是创意。温度高了模型容易输出格式混乱的内容。第二max_turns 限制。这是防止死循环的安全阀我见过模型陷入“调用工具→结果不对→再调用同一个工具”的死循环没有这个限制能把 API 额度跑光。第三错误处理。工具执行失败不能直接崩要把错误信息喂回模型让它自己决定下一步怎么办。3.4 上下文管理别让对话爆掉多轮对话之后messages 列表会越来越长最终超过模型的上下文窗口。这时候就需要截断策略。最简单的做法是保留 System Prompt 和最近 N 轮对话def truncate_messages(messages, max_tokens6000): if len(messages) 3: return messages system_msg messages[0] recent messages[-6:] # 保留最近 3 轮 # 粗略估算 token 数中文约 1.5 字/token total sum(len(m[content]) for m in [system_msg] recent) / 1.5 if total max_tokens: return [system_msg] recent # 还是超了进一步缩减 return [system_msg] messages[-4:]这个策略很粗糙但实用。更精细的做法是用 tokenizer 精确计算或者做摘要压缩。不过对于入门来说先跑通再说。我个人的经验是上下文管理是 Agent 从 Demo 到生产最大的鸿沟之一值得单独花时间研究。4. 进阶让 Agent 真正能干活4.1 多工具协作与任务规划单个工具调用只是起点。真实场景里用户的问题往往需要多个工具配合。比如“帮我查一下北京和上海今天的天气然后告诉我哪个更适合户外活动”这就需要先调用两次天气工具再让模型做比较。我试过两种方案。一种是让模型自己规划在 ReAct 循环里逐步调用。另一种是先让模型生成一个任务计划再按计划执行。前者更灵活后者更可控。实际用下来对于步骤明确的场景先规划再执行的成功率更高对于需要根据中间结果动态调整的场景ReAct 循环更合适。4.2 工具调用的容错与重试模型调用工具时参数格式错误是家常便饭。比如该传字符串的传了数字该传数组的传了单个值。我的做法是在工具执行前加一层参数校验def validate_args(tool, args): schema tool.parameters required schema.get(required, []) for key in required: if key not in args: return False, f缺少必需参数{key} return True, None校验失败时把错误信息喂回模型让它重新生成参数。实测下来大部分格式问题模型都能在第二次修正。如果连续三次都失败就放弃并返回错误提示。4.3 从单 Agent 到多 Agent 的演进思路当你手写完单 Agent 之后多 Agent 其实就是把多个单 Agent 用消息传递串起来。比如一个“研究员 Agent”负责搜索信息一个“写作 Agent”负责整理成文一个“审核 Agent”负责检查质量。每个 Agent 有自己的 System Prompt 和工具集通过一个调度器协调。但我要泼一盆冷水多 Agent 的复杂度是指数级上升的。调试难度大成本高而且很多场景下单 Agent 加多工具就能解决。我的建议是先把单 Agent 写到极致确实遇到瓶颈了再考虑多 Agent。5. 踩坑实录与排查速查表5.1 那些让我熬夜的典型问题问题一模型不按格式输出。明明在 Prompt 里写了要输出 JSON模型偏偏给你加一段解释文字。解决办法是在 Prompt 里加 few-shot 示例并且用temperature0。如果还不行就在解析函数里做容错用正则提取 JSON 部分。问题二工具调用死循环。模型反复调用同一个工具因为每次返回的结果它都觉得“不对”。这通常是因为工具返回的信息不够明确或者 Prompt 里没有告诉模型“什么情况下应该停止调用”。解决办法是在工具返回结果里加上明确的状态标识比如“查询成功结果为 XXX”或“查询失败原因 XXX”。问题三上下文超限。长对话之后模型开始胡言乱语因为上下文被截断了但模型不知道。解决办法是在 System Prompt 里明确告诉模型“如果之前的对话被截断请基于当前可见信息回答”。5.2 常见问题速查表现象可能原因排查方向解决方案模型不调用工具工具描述不清晰检查 description 是否说明触发条件补充“当用户询问 XXX 时调用”调用参数错误参数 schema 不明确检查 parameters 定义加 description 和示例值格式解析失败模型输出不稳定检查 temperature 和 Prompt加 few-shot降温度死循环缺少终止条件检查工具返回和 Prompt加 max_turns明确停止条件上下文超限对话轮次过多检查 messages 长度实现截断或摘要策略5.3 几个让我受益匪浅的实操心得心得一先写测试用例再写 Agent。我现在的习惯是先把期望的输入输出写成测试用例然后再去实现。这样每次改 Prompt 或改工具跑一遍测试就知道有没有退化。心得二日志要打全。Agent 的调试难度在于中间步骤多如果不打日志出了问题根本不知道是哪一步错了。我现在的做法是每一步的输入输出都记下来包括模型的原始回复、解析后的 action、工具的执行结果。心得三别迷信框架。手写一遍之后你会发现很多框架的设计其实有过度抽象的问题。理解原理之后你可以更理性地选择用不用框架、用哪个框架。6. 从手写 Agent 到生产级应用的差距6.1 并发与性能Demo 和线上的分水岭手写 Agent 跑通之后你可能会想“这不挺简单的吗”。但一旦要扛并发问题就来了。每次 Agent 调用可能涉及多轮模型请求和工具执行耗时从几秒到几十秒不等。如果同时来一百个请求你的服务直接崩。我试过的方案是异步化 队列。用 FastAPI 做接口层把 Agent 执行放到后台任务队列里前端轮询结果。这样虽然不能降低单次延迟但能保证服务不崩。另一个优化是缓存对于相同或相似的查询直接返回缓存结果省掉模型调用。6.2 可观测性出了问题怎么查生产环境的 Agent 必须可观测。我建议至少记录这几个指标每次请求的总耗时、模型调用次数、工具调用次数、失败率、平均轮次。这些数据能帮你快速定位问题——比如失败率突然上升可能是模型服务不稳定平均轮次增加可能是 Prompt 需要优化了。6.3 安全边界让 Agent 知道什么不能做Agent 能调用工具就意味着它能产生实际影响。如果工具里有“发送邮件”“删除文件”这类操作必须加权限控制。我的做法是在工具注册时加一个dangerous标记执行前检查用户权限。另外System Prompt 里要明确写清楚 Agent 的能力边界防止它被诱导执行危险操作。7. 后续可以怎么继续深入手写 Agent 只是一个起点。如果你已经跟着走完了这一遍接下来有几个方向可以继续挖。一是深入 Function Calling 的底层机制看看模型是怎么决定调用哪个工具的这对优化 Prompt 很有帮助。二是研究上下文压缩技术比如用模型自己做摘要把长对话压缩成短摘要这样能支持更长的对话。三是尝试多 Agent 协作但记得我前面说的先把单 Agent 写到极致。我个人在实际操作中的体会是手写 Agent 最大的价值不是让你“会写 Agent”而是让你对“模型能做什么、不能做什么、怎么让它做得更好”有了肌肉记忆。这种理解是看多少篇教程都换不来的。踩过几次坑之后你再看那些框架的文档会有一种“哦原来它是在解决这个问题”的顿悟感。这个感觉挺爽的。