做了一年多AI智能体项目从最初只会调大模型接口写点提示词到后来把记忆、工具调用、任务规划真正串成一套能落地的系统整个过程踩过的坑比学到的概念多得多。这篇东西不打算堆砌名词就是把“AI智能体项目怎么从零开始做”这件事用我自己实操过的路径完整讲一遍适合正在准备上手Agent开发的工程师也适合技术负责人评估项目技术方案时快速建立体感。先给没接触过的朋友一个定位。AI智能体不是简单的聊天机器人它是能感知用户目标、拆解任务、调用外部工具、观察执行结果、动态调整策略直到完成目标的一套系统。换句话说聊天机器人只会“说”智能体要“干”。这篇博文围绕这个核心展开分别讲项目思路拆解、关键模块设计、完整代码实现思路以及我在实际开发中遇到的问题和排查方法。1. 项目思路拆解AI智能体到底在解决什么问题1.1 从“问答机器人”到“会干活的智能体”早期做AI应用最常见的形式就是问答机器人。用户问一句模型答一句交互逻辑是线性的。这种结构处理信息查询类需求没问题比如“帮我解释一下什么是RESTful API”模型可以直接给出答案。但一旦需求变成“帮我查一下这周去某地的航班综合价格和时间挑一个最优的再顺便把目标地未来三天的天气告诉我最后整理成一份出行建议”线性问答就彻底不够用了。原因很简单模型拿不到实时数据没法执行查询动作更没法把多个步骤串成一个连贯的任务流。AI智能体的本质变化就在这里。它将一个复杂目标转化为一系列可执行的动作。整个系统是一个“感知-决策-行动”的闭环先理解用户意图再把意图拆解成子任务然后调用合适的工具去执行最后观察执行结果判断任务是否完成没完成就继续调整。这套闭环逻辑才是智能体和聊天机器人的根本分界线。1.2 为什么不直接套用现成Agent框架很多朋友一开始会纠结市面上有各种Agent框架和低代码平台拿来即用不就行了我的经验是区分场景。如果是做技术验证、Demo演示或者团队没有专门的AI工程师用现成产品没问题很快就能看到效果。但如果是面向真实生产环境的项目情况就变了。现成框架最大的问题不是功能少而是不可控。你必须接受它的上下文管理策略、工具调用规则、记忆存储方式一旦这些内部逻辑跟你的业务不匹配调起来非常痛苦。我见过一个团队为了在现成框架里做一个很简单的自定义数据库查询工具花了整整两周去摸框架的定制接口。另外现成框架往往封装得很重底层数据流不方便观察。而Agent项目最需要的就是可观察性。模型为什么调用这个工具参数是从哪里提取的哪一步产生了错误这些问题在自研架构里一条日志就能定位在别人封装好的黑盒里就只能靠猜。所以我自己的项目里选择了基于大模型API自研轻量级Agent框架。核心逻辑并不复杂也就一个主循环加工具注册机制。这个决策给后续开发带来的好处非常直接想加什么工具就加什么工具想怎么管理上下文就怎么管理出了问题时全链路日志一目了然。这里说明一下这是基于我自己的项目实践得出的选择如果你的团队情况不一样完全可以有自己的判断。1.3 整体架构与模块划分一个能落地的AI智能体项目我习惯分成五层层级职责关键组件交互层接收用户输入、返回结果Web接口、命令行入口、即时通信接入Agent核心解析意图、维护对话状态、调度工具主循环、意图解析器、状态管理器工具层提供可执行的具体能力函数注册表、参数校验、执行器记忆层管理短期会话和长期用户偏好上下文管理器、摘要存储模型层封装大模型推理能力API客户端、提示词模板各层之间保持单向依赖。模型层在最底部Agent核心通过模型层接口调用推理能力工具层只暴露注册和执行接口交互层完全不感知内部逻辑。这种分层的好处是替换成本低比如想从模型A切到模型B只改模型层封装就行其它层代码完全不用动。还有一点很重要每一层都要有日志输出。尤其是Agent核心这个位置任何一次工具调用、任何一次模型返回都要记录下来。AI项目的诡异问题太多了没有日志几乎等于瞎猜。2. 核心细节解析几个绕不开的关键设计2.1 模型层选型标准与参数调优模型选型是决定项目上限的一步。我不是说参数越大越好而是要看模型是否适合做Agent底座。我选择模型就盯三个指标。第一是函数调用能力。这个决定了模型能不能准确理解工具描述、正确输出要调用的函数名和参数。函数调用能力差的模型经常会把参数名改掉甚至把工具描述当成系统指令去执行。第二是上下文窗口大小。窗口决定了短期内能容纳多少轮对话和工具返回结果。但一定记住窗口大不代表就要把窗口塞满因为上下文越长、推理耗时和成本增长越明显。第三是指令遵循能力。Agent场景下的系统提示词通常很长包含角色定义、工具清单、输出规范、禁止事项模型能不能不打折扣地执行直接影响任务完成率。参数调优这块我一直强调temperature要设置很低建议0.1-0.2之间。原因是Agent需要的是确定性执行不是一个充满创意的发散回答。你可以把temperature想象成抽卡的随机率调得越低每次输出的结果越稳定。做工具参数提取这种任务时稍微高一点的temperature都可能导致参数值漂移。另外建议关闭流式输出做内部处理或者对流式内容做完整拼接再解析。因为Agent主循环依赖结构化返回流式半截数据无法判断是否已经稳定生成了工具调用指令。2.2 工具层函数调用的调用机制函数调用机制是整个智能体的手和脚。很多初学者会误以为模型真的能去执行函数其实不是。模型只是阅读你提供的工具说明书然后返回一个结构化的调用请求真正执行的是你自己的代码。也就是说你需要把工具描述清楚让模型知道什么时候用它、传什么参数。工具描述的质量直接决定调用准确率。写过一段时间工具后我总结出一套模板函数名要短语义要明确比如get_weather比query_weather_info_about_city这种又臭又长的名字好用得多。description字段要写清楚两个内容什么时候用什么时候不要用。举个例一个定义数据库查询工具的描述可以是“当用户询问订单、商品、用户资料等存放在数据库中的结构化数据时使用不要用于计算、文本生成等非查询类任务”。这个“不要用”的边界描述能显著降低误调用率。参数描述要具体到取值格式比如日期参数写明“格式为YYYY-MM-DD”枚举参数列出所有可选项。工具返回的数据也要讲究。我自己踩过一个坑让Agent查询数据库后直接返回完整JSON结果一个SQL查询可能返回几十行数据全部塞进上下文既浪费Token又容易让模型在后续推理中迷失重点。后来我统一在工具执行层做了一件事——格式化精简。返回结果只保留最关键的数据摘要转换为纯文本描述控制在几百字以内。模型拿到的是提炼后的信息而不是一堆原始JSON回复质量和稳定性都有明显提升。2.3 记忆层短期上下文与长期记忆如何配合记忆层的设计决定了Agent能不能“记得住”。短期记忆和长期记忆是两码事要区别对待。短期记忆是当前对话上下文中保留的消息列表。模型每多一轮交互消息列表就膨胀一分。如果无脑把全部历史都塞给模型很快两个问题就会出现查询成本飙升和模型注意力分散。我的做法是做一个滑动窗口。只保留最近若干轮消息更早的内容定期生成一段摘要把关键事实压缩进摘要里然后放在系统提示词中。这样模型既能记住重要信息又不会淹没在冗长的聊天记录里。这里给一个具体的数据参考。我一般把窗口设置成保留最近6-8轮消息摘要追加到系统提示词末尾。对于一般的任务型对话这个配置能平衡记忆质量和Token消耗。长期记忆解决的是跨会话记住用户偏好的问题。比如用户上次提到“我常驻上海出差一般选经济型酒店”下次会话Agent如果还能参考这个偏好体感就会完全不同。实现长期记忆不一定要上向量数据库。对小体量项目先把结构化偏好存放在一个持久化存储里就够用了。每次构建系统提示词时把用户偏好作为约束条件填充进去。这比向量检索稳定得多因为偏好信息本来就是离散键值对不需要语义检索。当然如果做的是文档问答类Agent需要检索历史对话或知识库片段那引入向量检索才有价值。2.4 规划层如何让Agent自动拆解复杂任务规划层解决的是“先做什么、再做什么”的问题。业界最常用的模式是ReAct核心思路就是“思考-行动-观察”循环。模型先思考当前应该做什么然后决定调用哪个工具观察工具返回结果再进入下一轮思考。这个循环在主循环里本身就自然实现了。但对于多步骤任务还可以做更显式的规划。比如在系统提示词中要求模型先输出一个简要计划例如“第一步查天气第二步查航班第三步综合比较”然后按计划逐步执行。显式计划的好处是让模型的决策路径可读出了问题你能判断是规划的哪一步错了也能让用户看到Agent正在做什么增加可控感。不过要提醒一句规划层千万别写得太复杂。很多初级设计者喜欢让Agent先输出一个完整的JSON格式的任务清单再设计一个Plan任务管理器去监控执行。我在一个项目里试过这种方案结果是小任务还行复杂任务经常出现计划完不成或者计划内步骤前后矛盾。后来我改回轻量级思路不给Agent加太多结构性负担只让它每一步做一个小决策反而更稳定。3. 实操过程与核心环节实现3.1 环境准备与基础搭建开始写代码之前先把开发环境准备好。我自己用的是这样的组合Python 3.10以上版本使用兼容OpenAI接口规范的SDK依赖就装了最基本的两三个包。模型API的密钥和基础地址建议放到环境变量里不要写死在代码中。一个简单的配置文件长这样import os API_KEY os.getenv(MODEL_API_KEY, ) API_BASE os.getenv(MODEL_API_BASE, https://your-model-endpoint.example.com) MODEL_NAME os.getenv(MODEL_NAME, your-model-name)设置环境变量之后写一个最基础的模型调用封装。def chat_completion(messages, toolsNone): from openai import OpenAI client OpenAI(api_keyAPI_KEY, base_urlAPI_BASE) params { model: MODEL_NAME, messages: messages, temperature: 0.2, } if tools: params[tools] tools params[tool_choice] auto response client.chat.completions.create(**params) return response.choices[0].message这段代码是整个项目的地基。tools参数不带时它就是普通对话接口带上时模型返回的结果里就可能包含工具调用请求后续主循环会用到。3.2 工具函数的定义与注册工具层是Agent能力的边界我建议从最简单的两个工具开始做通全链路后续再横向扩展。这里定义两个工具一个查天气一个查商品信息。先写好真实的执行函数。def get_weather(city: str) - str: # 真实项目中这里会调用天气服务接口这里做演示返回模拟数据 mock_data {city: city, weather: 晴, temperature: 26} return f{mock_data[city]}当前天气{mock_data[weather]}气温{mock_data[temperature]}摄氏度 def query_product(product_id: str) - str: # 真实项目中这里会查询商品数据库或者商品服务接口 mock_data {id: product_id, name: 示例商品, price: 199.0} return f商品{product_id}{mock_data[name]}当前售价{mock_data[price]}元然后是为这两个工具写模型描述。模型不看你Python函数的源码它只看这份JSON格式的工具说明书。tools [ { type: function, function: { name: get_weather, description: 查询指定城市当前天气当用户询问天气、温度、是否需要带伞等信息时使用, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如北京、上海、广州 } }, required: [city] } } }, { type: function, function: { name: query_product, description: 查询商品的基本信息当用户询问商品价格、名称、库存时使用, parameters: { type: object, properties: { product_id: { type: string, description: 商品编号通常是数字字符串 } }, required: [product_id] } } } ]注意我用一个字典把这个配置保存下来同时真实函数也注册进映射表。后面执行工具时从这个映射表查函数句柄这样既安全又方便扩展。TOOL_FUNCTIONS { get_weather: get_weather, query_product: query_product, }这个设计有个很实际的好处。以后每新增一个工具只需要写真实执行函数、配置JSON描述、注册进分发表三处改动就能完成扩展Agent核心代码完全不用动。3.3 Agent主循环的实现主循环是整个Agent的心脏。它的逻辑说起来很简单把用户输入和工具定义一起发给模型模型如果返回了工具调用请求就执行对应工具把执行结果作为新的消息回传给模型再让模型继续决策模型如果没有返回工具调用请求说明它已经得到结果或者不需要工具它的回复就是最终答案。我直接贴出核心实现的思路代码。class SimpleAgent: def __init__(self, system_prompt, tools, tool_functions): self.system_prompt system_prompt self.tools tools self.tool_functions tool_functions self.history [] self.max_history 8 # 滑动窗口大小 def _build_messages(self, user_input): # 从历史中截取最近N条加上系统提示词和当前输入 recent_history self.history[-self.max_history:] messages [{role: system, content: self.system_prompt}] messages.extend(recent_history) messages.append({role: user, content: user_input}) return messages def _execute_tool(self, tool_name, arguments): import json # 参数校验和非法工具拦截 if tool_name not in self.tool_functions: return f错误未知的工具{tool_name} try: args json.loads(arguments) if isinstance(arguments, str) else arguments func self.tool_functions[tool_name] result func(**args) return str(result) except Exception as e: return f工具执行出错{str(e)} def run(self, user_input): messages self._build_messages(user_input) max_rounds 6 # 最大循环轮数防止无限调用 for _ in range(max_rounds): response_message chat_completion(messages, toolsself.tools) messages.append(response_message) # 如果模型没有请求工具说明可以给用户最终答复 if not response_message.tool_calls: assistant_reply response_message.content or self.history.append({role: user, content: user_input}) self.history.append({role: assistant, content: assistant_reply}) return assistant_reply # 处理工具调用请求 for tool_call in response_message.tool_calls: tool_name tool_call.function.name arguments tool_call.function.arguments result self._execute_tool(tool_name, arguments) # 把工具结果作为消息追加回上下文 messages.append({ role: tool, tool_call_id: tool_call.id, content: result }) return 抱歉任务步骤过多未能完成处理这段代码有几个必须注意的设计点。第一是最大循环轮数。没有这个限制一旦模型陷入反复调用工具的怪圈比如反复查同一个城市天气程序会没完没了地跑下去消耗大量时长和费用我之前在测试阶段就遇到过模型连续调用十几次工具的情况。现在统一设置6轮上限超出就安全退出。第二是工具执行要捕获异常。大模型生成的参数不保证一定合法比如日期格式不对、城市名写成了数字工具函数要做防御性处理。有一次我遇到模型把城市参数传成一个包含城市名的长字符串当时没有异常捕获整个Agent直接崩溃了。加上异常处理后模型出错时对话仍然能继续只是答复质量下降不会整个服务挂掉。第三是注意消息格式的精确性。当模型返回工具调用时它的消息里会带一个tool_calls列表里面每个调用都有唯一的tool_call_id。回填工具结果时消息角色必须设置为tool并且带上这个ID模型才能把工具结果跟之前的工具调用关联起来。这个细节不处理好模型会完全看不懂工具返回的是谁的结果。3.4 加入记忆摘要与用户偏好单纯滑动窗口有个问题如果关键信息发生在窗口之外的早期对话里模型就忘了。所以还要配合摘要机制。摘要思路并不复杂。每经过一定轮数对话后把当前已有的消息列表发送给模型要求它输出一段简洁的摘要提取关键事实和用户偏好然后存到外部文件里。下次构建系统提示词时把摘要加入其中。def generate_summary(messages): prompt 请阅读以下对话提取其中的关键事实、用户偏好、待办事项用简洁的中文输出摘要\n\n prompt \n.join([f{m[role]}: {m.get(content, )} for m in messages]) # 用一个不带工具的调用生成摘要 summary_msg {role: user, content: prompt} resp chat_completion([summary_msg]) return resp.content def load_preferences(): # 从本地持久化文件读取用户偏好 import json, os if os.path.exists(preferences.json): with open(preferences.json, r, encodingutf-8) as f: return json.load(f) return {} def save_preferences(pref): import json with open(preferences.json, w, encodingutf-8) as f: json.dump(pref, f, ensure_asciiFalse, indent2)系统提示词的构建逻辑也同步调整def build_system_prompt(): prefs load_preferences() parts [ 你是一个实用的任务型智能助手能够调用工具来完成用户的请求。, 请严格按照以下约束执行, 1. 用户询问实时数据时必须调用对应工具获取不要凭记忆回答。, 2. 工具调用结果以实际返回为准不要编造结果。, 3. 当多个任务需要完成时逐步调用工具并汇总最终结论。, 4. 回答要简练直接给出结论和关键信息。, 5. 如果工具执行失败如实告知用户并提供下一步建议。 ] if prefs: pref_text .join([f{k}为{v} for k, v in prefs.items()]) parts.append(f6. 已知用户偏好{pref_text}在决策中优先参考。) return \n.join(parts)这个阶段跑通后你已经拥有一个具备工具调用能力和基础记忆的AI智能体了能处理“帮我查天气/查商品”这类跨领域实时需求也能记住用户一些简单偏好。预算充足或者数据量上来之后可以把偏好存储升级成数据库表把摘要检索升级成向量检索底层的Agent核心逻辑不需要改动。3.5 测试与效果调优测试Agent项目跟测试传统API项目完全不同。传统接口测试的输入输出是确定的而Agent输出有随机性甚至可能出现不同轮次行为不一致的情况。我做了一套简单可复用的测试流程。第一批是功能正确性测试准备一份手工用例清单覆盖工具调用的各种场景单工具查询用户问“北京今天天气怎么样”期望模型调用get_weather并返回城市天气。多工具连续查询用户问“查一下商品A01的价格顺便看看某个城市的天气”期望模型连续调用两个工具。未触发工具场景用户问“你好介绍一下你自己”期望模型不调用工具直接回复。每个用例跑三遍因为同一输入在不同轮次可能表现不同三遍通过两遍才算稳定。第二批是回归数据集。积累五到十个历史问题把期望行为写成测试脚本。后面每次调整提示词或者工具描述都跑一遍回归集防止改一个工具描述导致另一个工具调用全部崩掉。效果调优的方向我总结了一句经验优先调系统提示词和工具描述其次调模型最后才调代码逻辑。很多时候Agent表现不好不是因为代码有Bug而是模型没有得到足够清晰的指令。比如模型反复把日期参数传错格式那就在工具描述里写“日期参数只接受YYYY-MM-DD格式”模型容易编造工具不存在的返回结果就在系统提示词里加一条“不得编造工具调用结果”。4. 常见问题与排查技巧实录4.1 高频异常现象与根因分析开发和实际使用Agent项目时我整理过一份问题清单全是实战中遇到过的真实情况。现象根因对应策略模型反复调用同一个工具不输出最终答案工具描述没有说明“何时停止”或温度偏高调低temperature在描述里加“获取结果后直接回复用户”工具参数解析失败或传错参数描述不清晰模型没有理解取值格式细化参数说明注明格式、枚举值、默认值对话超过十几轮后模型开始丢失上下文滑动窗口策略缺失历史消息塞爆上下文实现滑动窗口和摘要压缩机制模型不调用工具直接编造实时数据工具描述不够醒目模型忽略了tools参数加强系统提示词中“实时数据必须调用工具”的约束工具返回结果很大回复质量下降原始数据未经提炼直接入上下文工具层做结果精简只保留关键信息Agent运行很久不返回疑似死循环缺少最大轮数限制在Agent主循环里设置调用轮数上限4.2 排查问题的基础思路日志先行Agent项目排查问题和传统开发最大的不同是错误往往不是异常崩溃而是“逻辑错误但程序正常”。比如模型第三次调用工具时传错了一个参数程序完全没报错但流程已经跑偏。没有日志你根本不知道哪一步拐弯了。我在Agent核心的每个关键节点都加了日志。用户在什么时间输入了什么内容模型返回了什么消息是否有工具调用工具名称和参数是什么工具返回了什么结果最终答复是什么全部打点记录。每一轮工具的调用路径都能完整复现。有了日志后排查一个Agent问题就像回放录像一样把调用链路拉出来一眼就能看到模型在哪一步产生了错误的参数或者在哪一步突然决定停止调用工具。有一次一个Agent任务完成率掉到一半以下我通过日志发现是新增的一个工具描述跟已有工具描述高度相似模型经常选错。改掉描述后效果立刻恢复。4.3 几条独家避坑心得最后分享几条我不会放在代码注释里的经验。第一不要让工具函数返回一大坨JSON。模型处理长JSON的效率和准确度都对不起你花出去的Token钱。工具在返回前就应该把字段提炼成短文本尤其是数据库查询类工具只回传几行关键结果。实测同样的任务把工具返回体从JSON改成精简文本后响应速度提升很明显而且后续步骤的准确性也变高了。第二第一版Agent的工具数量必须严格控制。你有二十个工具想接进去先忍一忍。工具越多模型选错工具的概率越大调试时的变量也越多。先把三到五个核心工具做成高质量跑通全链路再慢慢往里加。工具超过十五个之后模型调用准确率会明显下降这时就得考虑工具分组或做工具路由了。第三给所有外部依赖做兜底包括模型API超时、工具接口不可用、返回格式异常。一个真实案例是某个天气接口偶尔返回数据缺失Agent拿到残缺数据后开始幻觉补全编造出根本不存在的温度值。后来我在工具层主动检查返回完整性缺失字段直接返回“数据不可用”模型就不会继续编故事了。第四模型API版本升级前先在测试环境验证工具调用兼容性。我遇到过API版本升级后函数调用参数格式发生细微变化并没有通知所有用户结果线上Agent突然大面积工具调用失败。升级开放限制后这一切其实可以提前发现。结尾我自己做了几个AI智能体项目之后最深的体会是这个方向真正花精力的地方不在模型有多聪明而在怎么把系统的边界和约束设计好。模型像一把锋利的刀你得给它配一个顺手的刀鞘和安全的操作规则它才能在真实场景里稳定发挥。先把最小闭环跑通再考虑加更多工具和更花哨的规划能力这个节奏对于所有想入局的开发者来说应该是最稳妥的路径。最后再分享一个已经养成习惯的小技巧从第一个版本开始就把每次工具调用的耗时、Token消耗量记录到日志里。这些数据在后续做模型选型、优化提示词、评估成本时都是最有说服力的依据不要等到项目跑大了才想起来补数据。