
AI Agent 这个词这两年火得离谱但大部分文章都在讲概念、画架构图真正动手从零写一个的人并不多。我用 Python 从零搭过几个 Agent踩了不少坑也积累了一些实战经验。这篇文章不讲虚的直接聊怎么从零开始构建一个能跑的 AI Agent为什么值得自己动手写而不是直接用现成框架以及在这个过程中会遇到哪些实际问题。适合有一定 Python 基础、想真正理解 Agent 底层运作机制的开发者也适合那些用现成框架遇到瓶颈、想深入定制的人。全文会围绕AI Agent、Python、Anthropic API和from Scratch这几个核心关键词展开把每个环节的决策逻辑和实操细节都讲透。1. 为什么值得从零构建一个 AI Agent1.1 现成框架的便利与局限市面上已经有不少 Agent 框架比如 LangChain、AutoGPT、CrewAI 等它们确实能让你在几分钟内跑起来一个看起来像 Agent的东西。但用了一段时间后你会发现几个很现实的问题。第一是黑盒感太强。框架帮你封装了 prompt 拼接、工具调用、记忆管理、循环控制等环节一旦行为不符合预期你很难定位到底是哪一层出了问题。我遇到过 Agent 反复调用同一个工具、陷入死循环的情况翻文档翻了半天也没找到根因最后还是得去读源码。第二是抽象层过多导致调试困难。一个简单的让 Agent 查天气然后决定穿什么衣服的任务在框架里可能涉及 Chain、AgentExecutor、Tool、Memory 等好几个抽象层。每层都有自己的配置项和默认行为叠加起来就变成了一个复杂的系统。出了问题你不知道该从哪一层开始排查。第三是定制成本高。当你需要实现一个非标准的控制流比如先让 Agent 规划步骤人工审核后再执行或者根据任务复杂度动态切换模型在框架里往往要绕很多弯甚至要继承和重写大量内部类。从零构建的好处在于你完全掌控每一行代码。prompt 怎么拼、工具怎么调、循环什么时候停、错误怎么处理全部由你决定。这种掌控力在调试和定制时价值巨大。1.2 从零构建能让你真正理解什么自己写一个 Agent你会被迫面对几个核心问题LLM 到底是怎么决定调用工具的答案其实很简单靠 prompt 里的格式约定和模型的指令遵循能力。没有什么魔法就是你在 system prompt 里告诉它如果要调用工具请按这个 JSON 格式输出然后模型照做。Agent 的循环是怎么控制的本质上就是一个 while 循环调用模型 → 解析输出 → 如果是要调工具就执行 → 把结果塞回对话历史 → 再次调用模型 → 直到模型输出最终答案或达到最大轮次。记忆是怎么实现的最基础的就是维护一个消息列表每次调用模型时把整个列表传进去。所谓长期记忆不过是把这个列表持久化到数据库或文件里。Token 消耗在哪里每一轮循环都会把完整对话历史发给模型所以对话越长token 消耗越大。这是 Agent 成本控制的核心问题。理解了这些你再用任何框架都能一眼看穿它的本质遇到问题也知道该往哪个方向排查。1.3 适合从零构建的场景不是所有场景都适合从零写。如果你的需求是快速验证一个想法用框架可能更高效。但以下场景从零构建明显更合适需要深度定制控制流比如多阶段人工审核、条件分支、动态规划需要精确控制 token 消耗比如对成本敏感的生产环境需要集成私有工具链且这些工具的调用逻辑比较特殊想要学习 Agent 底层原理为后续技术选型打基础需要极致的性能和轻量化不想引入庞大的依赖我个人的经验是先用从零构建的方式写一个最小可用版本理解清楚每个环节然后再决定是否引入框架来加速开发。这样即使后面用框架你也能清楚地知道框架在帮你做什么。2. 核心架构设计与技术选型2.1 Agent 的最小核心循环一个 AI Agent 的本质可以用一句话概括在循环中调用 LLM让它根据当前状态决定下一步动作直到任务完成。这个循环包含几个关键环节接收任务用户输入一个目标构建 prompt把系统指令、可用工具描述、对话历史拼成一个完整的 prompt调用 LLM把 prompt 发给模型获取输出解析输出判断模型是想调用工具还是给出了最终答案执行动作如果是工具调用执行对应函数拿到结果更新状态把工具结果追加到对话历史循环判断如果任务未完成且未超过最大轮次回到第 2 步这个循环看起来简单但每个环节都有很多细节需要处理。比如 prompt 怎么设计才能让模型稳定地按格式输出工具调用的参数解析失败了怎么办模型陷入死循环怎么检测2.2 为什么选择 Anthropic API在模型选型上我最终选择了 Anthropic 的 API主要基于以下几点考虑工具调用Tool Use的原生支持。Anthropic 的 API 对工具调用有非常清晰的设计你在请求里定义 tools 数组模型如果决定调用工具会在返回的 content 里包含一个tool_use类型的块里面结构化了工具名称和参数。这比自己用 prompt 约定 JSON 格式要可靠得多解析起来也简单。长上下文窗口。Agent 的对话历史会随着循环不断增长长上下文能力直接决定了 Agent 能处理多复杂的任务。Anthropic 的模型在这方面表现不错能支撑较长的多轮工具调用。指令遵循能力强。Agent 的核心依赖就是模型能否严格按照你的指令行事包括格式要求、工具选择逻辑、停止条件等。实测下来Anthropic 的模型在这方面的稳定性让我比较满意。API 设计简洁。请求和响应的结构都很清晰没有太多历史包袱上手快。当然这不是说其他模型不能用。如果你用的是其他家的 API核心逻辑是一样的只是请求和响应的格式需要调整。文章里我会以 Anthropic API 为例但思路是通用的。2.3 技术栈与依赖选择整个项目我尽量保持轻量核心依赖只有几个依赖用途选择理由anthropic调用 Claude 模型官方 SDK稳定可靠python-dotenv管理 API Key避免密钥硬编码rich终端输出美化调试时看得清楚pydantic数据校验工具参数校验可选不需要 LangChain不需要任何 Agent 框架。Python 版本建议 3.10 以上因为要用到一些新的类型语法。如果你还在用 3.8大部分代码也能跑但类型提示部分需要调整。安装依赖很简单pip install anthropic python-dotenv rich pydanticAPI Key 放在.env文件里ANTHROPIC_API_KEYyour_key_here然后在代码里加载from dotenv import load_dotenv load_dotenv()这样做的好处是密钥不会出现在代码里也不会被误提交到版本控制。记得把.env加到.gitignore里。3. 从零实现 Agent 的核心模块3.1 工具系统的设计与实现工具是 Agent 能力的延伸。没有工具Agent 只能聊天有了工具它才能查数据、调 API、操作文件、执行计算。设计工具系统时我遵循几个原则每个工具是一个独立的函数职责单一输入输出明确。比如一个查天气的工具输入是城市名输出是天气信息字符串。工具的描述要写给模型看。模型是根据工具的名称和描述来决定是否调用的所以描述必须清晰、准确说明这个工具能做什么、什么时候该用、参数是什么含义。参数校验要在执行前做。模型生成的参数不一定总是合法的可能是类型错误、缺少必填项、或者值超出范围。在执行工具前做一层校验能避免很多莫名其妙的错误。下面是一个工具的定义示例from anthropic.types import ToolParam def get_weather(city: str) - str: 查询指定城市的当前天气 # 实际实现会调用天气 API weather_data { 北京: 晴气温 25°C湿度 40%, 上海: 多云气温 28°C湿度 65%, } return weather_data.get(city, f未找到 {city} 的天气数据) # 工具的 schema 定义告诉模型这个工具怎么用 weather_tool: ToolParam { name: get_weather, description: 查询指定城市的当前天气情况。当用户询问天气相关问题时使用此工具。, input_schema: { type: object, properties: { city: { type: string, description: 城市名称例如北京、上海 } }, required: [city] } }这里的关键是input_schema它用 JSON Schema 的格式描述了工具的参数。模型会根据这个 schema 生成符合格式的参数。schema 写得越清晰模型生成错误参数的概率越低。我建议给每个工具都写详细的 description包括使用场景。比如不要只写查询天气而要写查询指定城市的当前天气情况。当用户询问天气、气温、是否下雨等问题时使用此工具。这样模型能更准确地判断何时该调用。3.2 对话历史与记忆管理Agent 的记忆本质上就是一个消息列表。每次调用模型时把整个列表传进去模型就能看到之前发生了什么。消息列表的结构大致是这样的messages [ {role: user, content: 帮我查一下北京和上海的天气}, {role: assistant, content: [ {type: tool_use, id: call_1, name: get_weather, input: {city: 北京}} ]}, {role: user, content: [ {type: tool_result, tool_use_id: call_1, content: 晴气温 25°C} ]}, # ... 继续循环 ]这里有几个关键点工具调用的结果要以tool_result的形式返回并且要带上对应的tool_use_id。这样模型才能把结果和之前的调用对应起来。消息列表会不断增长。每一轮工具调用都会增加至少两条消息一条 assistant 的 tool_use一条 user 的 tool_result。对话越长token 消耗越大。需要设置历史截断策略。当消息列表太长时可以选择保留最近的 N 条或者对早期消息做摘要。最简单的做法是设置一个 token 上限超过就丢弃最早的消息。但要注意丢弃消息可能让 Agent 忘记之前的上下文所以截断策略要根据任务特点来定。我通常的做法是保留 system prompt 和最近若干轮对话中间的历史如果太长就做摘要。摘要可以用模型来做也可以用简单的规则比如只保留工具调用的关键结果。3.3 主循环的实现与终止条件主循环是整个 Agent 的心脏。它的逻辑是不断调用模型执行工具直到模型给出最终答案或达到终止条件。import anthropic client anthropic.Anthropic() def run_agent(task: str, tools: list, tool_functions: dict, max_turns: int 10): messages [{role: user, content: task}] for turn in range(max_turns): response client.messages.create( modelclaude-sonnet-4-20250514, max_tokens4096, system你是一个有用的助手可以使用工具来完成任务。, toolstools, messagesmessages ) # 把模型的回复加入历史 messages.append({role: assistant, content: response.content}) # 检查是否有工具调用 tool_uses [block for block in response.content if block.type tool_use] if not tool_uses: # 没有工具调用说明模型给出了最终答案 final_text .join( block.text for block in response.content if block.type text ) return final_text # 执行所有工具调用 tool_results [] for tool_use in tool_uses: func tool_functions.get(tool_use.name) if func: try: result func(**tool_use.input) except Exception as e: result f工具执行错误{str(e)} else: result f未知工具{tool_use.name} tool_results.append({ type: tool_result, tool_use_id: tool_use.id, content: str(result) }) messages.append({role: user, content: tool_results}) return 达到最大轮次限制任务未完成这段代码有几个值得注意的地方终止条件有两个一是模型不再调用工具给出最终答案二是达到max_turns。第二个条件是必须的否则模型可能陷入无限循环。工具执行要包在 try-except 里。工具可能因为各种原因失败网络问题、参数错误、外部 API 异常失败时不要把整个 Agent 搞崩而是把错误信息作为工具结果返回给模型让模型决定怎么处理。多个工具调用要全部执行。模型可能在一轮里同时调用多个工具比如同时查北京和上海的天气。这些调用要全部执行结果一起返回。消息顺序很重要。assistant 的 tool_use 消息后面必须紧跟 user 的 tool_result 消息否则 API 会报错。3.4 System Prompt 的设计要点System prompt 是 Agent 的行为准则它决定了 Agent 的性格、能力边界和输出风格。设计 system prompt 时我通常会包含以下几部分角色定义告诉模型它是谁比如你是一个专业的天气助手。能力说明告诉模型它有哪些工具可用以及什么时候该用哪个工具。行为约束告诉模型什么该做、什么不该做。比如如果用户的问题超出你的能力范围请如实告知。输出格式要求如果需要特定的输出格式在这里说明。一个实际的 system prompt 示例你是一个智能助手可以使用工具来帮助用户完成任务。 可用工具 - get_weather查询城市天气。当用户询问天气相关问题时使用。 - search_web搜索网络信息。当需要查找最新信息时使用。 行为准则 1. 优先使用工具获取准确信息不要凭记忆回答事实性问题。 2. 如果工具返回错误尝试其他方式或如实告知用户。 3. 完成任务后用简洁的语言总结结果。 4. 不要编造工具没有返回的信息。这里的关键是明确工具的适用场景。模型需要知道什么时候该调用工具什么时候不该。如果描述模糊模型可能在不该调用的时候调用或者该调用的时候不调用。4. 实操中的常见问题与排查技巧4.1 模型不调用工具怎么办这是最常见的问题之一。你定义了一个工具但模型就是不用直接凭自己的知识回答。原因通常有几个工具描述不够清晰。模型不知道这个工具是干什么的或者不知道什么时候该用。解决方法是把 description 写得更具体明确使用场景。System prompt 没有强调要用工具。如果 system prompt 里没有明确说优先使用工具模型可能觉得直接回答也行。加一句对于事实性问题优先使用工具获取准确信息往往能解决。工具名称太抽象。query_data不如get_weather直观。工具名称要能让人一眼看出它是干什么的。模型本身的能力限制。有些模型对工具调用的支持不够好换一个工具调用能力更强的模型可能更有效。我的经验是工具描述要写到一个新人看了也知道什么时候用的程度。不要假设模型能理解你的意图要把使用场景明确写出来。4.2 工具参数解析失败的排查模型生成的参数可能不符合 schema比如该传字符串的传了数字该传数组的传了单个值。排查步骤打印模型返回的原始 tool_use 块看看参数到底是什么样的检查 schema 定义是否清晰特别是类型和描述在 schema 里加 examples给模型一个参考在执行前做参数校验不合法就返回错误信息让模型重试一个实用的技巧是在 schema 的 description 里给出示例值city: { type: string, description: 城市名称例如北京、上海、广州 }这样模型生成参数时会有更明确的参考。4.3 死循环的检测与处理Agent 陷入死循环是很常见的问题。表现是模型反复调用同一个工具或者在不同工具之间来回切换但始终不给出最终答案。检测方法设置最大轮次限制这是最基本的保护检测重复的工具调用如果连续几轮调用同一个工具且参数相同可能是陷入了循环监控 token 消耗如果消耗异常增长可能有问题处理策略达到最大轮次时强制让模型给出当前最好的答案检测到重复调用时在对话里插入提示比如你已经调用过这个工具了请基于已有信息给出答案在 system prompt 里明确说不要重复调用同一个工具除非参数不同我遇到过一个案例Agent 反复查询同一个城市的天气因为它在等一个更新的结果。后来在 system prompt 里加了一句工具返回的结果就是当前最新数据不需要重复查询问题就解决了。4.4 Token 消耗过大的优化Agent 的 token 消耗主要来自两个方面对话历史的增长和工具返回结果的体积。优化方向问题优化方法对话历史过长截断早期消息或做摘要工具返回结果太大只返回关键信息截断长文本System prompt 太长精简描述去掉冗余模型输出太长在 prompt 里要求简洁输出我通常会设置一个 token 预算比如整个任务不超过 50K token。当接近预算时开始截断历史或压缩工具结果。还有一个技巧是工具返回结果时只保留模型需要的信息。比如一个搜索工具返回了 10 条结果但模型可能只需要前 3 条的摘要。在工具函数里做一层过滤能显著减少 token 消耗。4.5 常见问题速查表问题现象可能原因解决方法模型不调用工具描述不清、prompt 未强调完善工具描述在 system prompt 里明确要求参数格式错误schema 不清晰加 examples做参数校验死循环缺少终止条件设置 max_turns检测重复调用Token 消耗大历史过长、结果太大截断历史精简工具返回工具执行报错外部依赖问题try-except 包裹返回错误信息给模型模型输出格式不对prompt 未约束在 system prompt 里明确格式要求5. 进阶扩展与生产化考虑5.1 多 Agent 协作的雏形当你理解了单个 Agent 的运作方式多 Agent 协作就变得很自然了。本质上就是让一个 Agent 的输出成为另一个 Agent 的输入。最简单的多 Agent 模式是规划者 执行者一个 Agent 负责把复杂任务拆解成子任务另一个 Agent 负责执行每个子任务。规划者不需要工具只需要输出结构化的任务列表执行者拿到任务后调用工具完成。实现上你可以把run_agent函数复用两次只是传入不同的 system prompt 和工具集。规划者的输出解析成任务列表然后逐个交给执行者。更复杂的模式包括辩论式多个 Agent 给出方案互相评审和流水线式每个 Agent 负责一个环节串行处理。但要注意Agent 数量增加会带来 token 消耗的指数级增长实际使用时要权衡。5.2 持久化与状态恢复生产环境里Agent 的运行可能跨越很长时间需要支持中断和恢复。这就要求把对话历史和状态持久化。最简单的做法是把messages列表存成 JSON 文件或存到数据库。每次循环开始时加载结束时保存。这样即使进程重启也能从上次的状态继续。需要注意的点工具调用的中间状态要保存否则恢复后模型不知道之前调用了什么要有一个唯一的会话 ID用来区分不同的任务要考虑并发多个 Agent 同时运行时不能互相干扰我用 SQLite 做过一个简单的持久化方案一张表存会话元信息一张表存消息记录。恢复时按会话 ID 查询消息重建messages列表。对于大多数场景这已经够用了。5.3 安全边界与权限控制Agent 能调用工具就意味着它能产生实际影响。如果工具包括执行 shell 命令或发送邮件那安全边界就非常重要。几个基本原则最小权限只给 Agent 完成任务必需的权限。不需要写文件就不要给写文件的工具。人工审核对于高风险操作删除数据、发送消息、执行命令在执行前要求人工确认。输入校验工具的参数要严格校验防止注入攻击。比如执行 shell 命令的工具要过滤危险字符。操作日志记录 Agent 的每一步操作便于事后审计和排查。我在一个项目里做过这样的设计Agent 生成的操作先进入一个待审核队列人工确认后才真正执行。虽然增加了交互成本但对于涉及敏感操作的场景这是必要的。5.4 从脚本到服务的演进路径一开始Agent 可能只是一个 Python 脚本在终端里跑。要变成可用的服务需要几个步骤第一步封装成函数或类。把 Agent 的逻辑封装成一个类提供run(task)方法方便调用。第二步加一个简单的 Web 接口。用 FastAPI 或 Flask 包一层 HTTP 接口接收任务、返回结果。这时候要注意异步处理因为 Agent 运行可能耗时较长。第三步加任务队列。用 Redis 或 RabbitMQ 做任务队列支持并发处理和任务状态查询。第四步加监控和日志。记录每个任务的 token 消耗、执行时间、工具调用次数便于优化和排查。第五步加限流和配额。防止单个用户消耗过多资源。这个演进路径不是必须一步到位可以根据实际需求逐步推进。我个人的建议是先用脚本验证核心逻辑确认可行后再考虑服务化。过早引入复杂的架构反而会拖慢迭代速度。5.5 我踩过的几个坑最后分享几个实际踩过的坑希望能帮你少走弯路。坑一忽略了消息顺序的严格性。Anthropic API 要求 assistant 的 tool_use 消息后面必须紧跟 user 的 tool_result 消息。我有一次在中间插入了一条额外的 user 消息导致 API 一直报错。排查了很久才发现是消息顺序的问题。坑二工具返回了非字符串内容。tool_result的content字段要求是字符串但我有一次直接返回了一个字典导致序列化失败。后来统一在返回前做str()转换。坑三max_tokens 设置太小。如果模型需要输出较长的工具调用参数max_tokens太小会导致输出被截断参数解析失败。建议至少设置 4096。坑四没有处理工具超时。外部 API 调用可能很慢如果没有超时机制Agent 会一直卡在那里。给每个工具调用加一个超时限制是必要的。坑五system prompt 太长导致成本高。一开始我把所有工具的描述都塞进 system prompt导致每次调用都要传很多 token。后来改成只在 tools 参数里定义工具system prompt 只保留行为准则成本降了不少。这些坑看起来都是小问题但在实际开发中会浪费大量时间。希望你在构建自己的 Agent 时能避开它们。从零构建 AI Agent 的过程本质上是一个不断理解和调试 LLM 行为的过程。每解决一个问题你对 Agent 运作机制的理解就深一层。这种理解是任何框架文档都给不了的。