☰
从零构建AI智能体:核心组件、开发流程与稳定性实践
2026/10/14 10:25:14 网站建设 项目流程

“AI智能体”大概是最近一年里出现频率最高的词之一。但说句实在话,我在社区里看到的很多“智能体”项目,本质上是套了个壳的聊天机器人:一个大模型API配上一段系统提示词,能聊天、能回答,但没有循环、没有工具、没有状态管理,离真正的“智能体”还有很大距离。

这篇文章想分享的是我从零开始做智能体的完整开发流程,不是某个框架的教程,而是一条通用的构建链路:需求拆解、最小骨架、核心组件设计、实际案例、稳定性测试和排查经验。看完之后你会清楚一个智能体到底是什么、每一步该怎么落地、哪些环节最容易出问题。适合想做个人项目但不知道从哪下手的开发者,也适合已经在用编排框架但想知道底层原理的朋友。

1. 开工之前,先把“智能体”拆成四个零件

1.1 需求拆解:先定义任务,再决定要不要上智能体

很多项目一开始就跑偏,是因为没有做需求拆解就直接写代码。智能体本质上是一个“能自己决定下一步干什么”的程序,但这不是优点而是成本:它比普通脚本多了一层推理开销,也多了很多不可控因素。所以在动手前,我会先花半天时间把用户需求拆成最小任务单元。

举个例子,某开发者想做一个“自动整理会议纪要点”的智能体。拆完之后大概是这样的:

  • 录音转文字:根本不需要模型,直接调用成熟的转写服务就行;
  • 信息抽取:把发言内容里的关键结论抽出来,这需要模型;
  • 生成行动项:根据结论生成“谁在什么时候做什么”,这需要模型;
  • 写入项目管理工具:调用API写入,不需要模型。

拆完之后你会发现,真正需要“智能”的地方只有两个环节。其它部分用普通代码更便宜、更稳、更容易测试。这个判断标准很朴素:凡是输入输出关系固定、逻辑确定的环节,一律用代码;只有需要理解语义、动态决策、生成内容的环节,才用大模型。

我把适合用智能体的任务特征整理成了一个对照表,每次做需求评审都用它:

适合智能体的任务更适合普通脚本的任务
输入表达多样、没有固定格式输入结构固定、字段明确
需要多步判断和动态规划逻辑分支最多三五个
输出是自然语言或内容创作输出是精确数据或固定格式
需要调用多个外部工具组合完成只依赖单一数据源
需求本身还在演化中需求完全确定且长期不变

如果你发现任务大多落在右列,直接写个脚本就好,别硬上智能体。这是我最想强调的一点:技术选型不是越先进越好,而是越合适越好。

1.2 边界划定:什么该让模型做,什么不该让模型做

需求拆完之后,接下来是划分边界。我的方法很简单:把智能体想象成一个很聪明但偶尔会犯低级错误的实习生。这个实习生理解能力强、学习速度快,但你不敢让他直接动数据库,也不敢让他做精确到小数点后三位的计算,更不敢让他碰敏感权限。

具体来说,这三件事我会明确交给模型:

  • 意图识别:用户说“帮我看看这周的提交情况”,“这周”到底从哪里算起?这个语义理解交给模型;
  • 内容生成:把工具的返回数据整理成一篇清晰的日报、周报、总结,模型做;
  • 决策选择:根据当前状况决定下一步调用哪个工具、是否需要更多信息,模型做。

而这三件事我一定用代码兜底:

  • 精确计算:模型算数不靠谱,凡是涉及数量、金额、日期的精确逻辑,全部包成工具,让代码返回确定结果;
  • 状态管理:智能体当前执行到哪一步、已经调用了哪些工具、还剩多少预算,这些状态放在程序里,不让模型自己记;
  • 敏感操作:删除、写入、支付、发消息这类动作,最多让模型“提出建议”,最终执行必须由程序确认。

有一回我做了一个数据整理智能体,它需要统计某目录下的文件数量。模型第一次回答“大约128个”,第二次又变成“约132个”。这就是典型的模型在脑补数据。后来我把统计逻辑包成了工具,让代码直接返回数字,模型只负责解读结果,这个问题就彻底消失了。凡是“错一次代价很大”的动作,默认不要交给模型自己做。

1.3 选型判断:从零写循环,还是直接上编排框架

边界划清后就是选型。市面上已经有不少智能体编排框架,它们封装了工具调用、记忆管理、任务规划这些能力,用起来确实方便。但我的建议是:第一个项目最好别直接用框架,自己从零写一个最小循环。

为什么?因为编排框架抽象程度高,出了问题你根本不知道是模型的问题还是框架的问题。自己写一遍循环,哪怕只有几十行代码,你会把整个链路理解得很透:模型返回了什么结构、工具结果怎么回传、上下文怎么拼接,这些都是排查问题的基础。

选型的判断点主要有三个:

  1. 项目复杂度:如果只是一个工具调用链,完全不需要框架;如果要做多智能体协作、复杂的任务规划、人机协同流程,用框架能省大量时间;
  2. 生产要求:如果要做高并发、频繁迭代的生产服务,框架的抽象能帮你统一处理重试、限流、日志;如果只是个人项目,自己写的循环更灵活;
  3. 团队水平:如果团队里的人都清楚原理,用框架是提效;如果大家不理解原理就上框架,出了问题只能瞎猜。

至于模型选型,核心就看三个能力:是否支持function calling(函数调用)、上下文窗口多大、价格和延迟是否可接受。function calling是刚需,没有它你很难稳定地让模型按格式输出工具调用指令。上下文决定了你一次能塞多少资料,价格和延迟则决定了你的方案能不能长期跑。

2. 搭建最小开发骨架:先跑通一个推理循环

2.1 环境准备与项目结构

第一个版本不需要复杂架构,一个Python文件就能跑。我的建议是先用Python 3.10以上的虚拟环境,把依赖做到最简。

python -m venv .venv source .venv/bin/activate pip install requests python-dotenv

这里我没有用任何框架,只装了HTTP请求库和dotenv。很多人觉得智能体项目一定很复杂,其实验证一个想法用不了多少依赖。项目结构也很简单:

agent_demo/ ├── .env # 存放API Key ├── agent.py # 主循环 ├── tools.py # 工具函数 └── prompt.py # 提示词模板

.env文件里只放一行:

LLM_API_KEY=你的密钥

然后写一个小工具函数读取配置。记住一点:API Key永远不要硬编码在代码里,也不要提交到代码仓库。我见过太多因为密钥泄露导致账单飙升的案例,这个坑踩一次就够心疼的了。

2.2 推理循环:智能体的心脏

智能体和普通聊天机器人的根本区别,在于它有一个推理循环。聊天机器人是“一句话进、一句话出”,而智能体是反复思考、调用工具、观察结果、再思考,直到得出最终答案。

这段代码是整个流程的核心,值得仔细看:

import json import os from dotenv import load_dotenv load_dotenv() # 这里用了一个抽象的客户端,实际接入时换成你选的模型SDK即可 from llm_sdk import LLMClient client = LLMClient(api_key=os.getenv("LLM_API_KEY")) SYSTEM_PROMPT = "你是智能体助手,你会使用工具完成任务。" MAX_STEPS = 8 def run_agent(user_query: str): messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_query}, ] for step in range(MAX_STEPS): print(f"===== Step {step + 1} =====") response = client.chat(messages=messages, tools=TOOLS) # 情况1:模型决定调用工具 if response.tool_calls: print(f"模型请求调用工具: {response.tool_calls}") messages.append(response.message) for tool_call in response.tool_calls: result = dispatch_tool(tool_call) print(f"工具返回: {result[:200]}...") messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result, }) continue # 情况2:模型直接给出最终回答 print(f"最终回答: {response.content}") return response.content return "达到最大步数,任务终止。"

这个循环里最关键的变量是MAX_STEPS。没有它,一个走岔路的智能体会在工具调用里无限循环下去,每次调用都是真金白银的成本。设成8或者10就够了,正常情况下一个任务三步五步能解决,超过这个数大概率逻辑有问题。

2.3 调试只能靠日志:先设计观测点再写功能

智能体开发最大的难点在于不确定性。普通程序每一步都是确定的,出错了看堆栈就行;智能体每一步都可能走不同分支,它的“心路历程”全在一连串大模型调用里。所以开发时第一优先级不是功能多炫,而是能不能看懂它在干什么。

我强烈建议在最开始就给循环加上日志:每轮打印当前步数、模型是否调用了工具、调用的哪个工具、参数是什么、工具返回了什么。别嫌日志啰嗦,真正调错的时候你会发现每条日志都是救命线索。

后来我把日志升级成了结构化输出,每一步记录以下字段:

字段说明示例
step当前推理步数2
action模型行为类型tool_call / final_answer
tool_name调用的工具名get_git_commits
input_summary工具入参摘要{"since": "2025-01-01"}
output_summary工具出参摘要返回17条记录
token_usage本轮token消耗2341

这些字段存成JSON写入本地日志文件或直接输出表格。有了这些数据,你才能回答两个核心问题:它做对了没有?如果没有,是哪一步开始走偏的?没有日志就调智能体,等于闭着眼开车,能跑全靠运气。

3. 三大核心组件设计:指令、工具、记忆

3.1 系统提示词:像写接口文档一样写Prompt

很多人的第一版系统提示词是“你是一个智能助手,请尽可能帮助用户”。这段话等于什么都没说。智能体项目的提示词不是作文,而是约束模型行为的接口文档。好的提示词应该让模型明确知道四件事:你是谁、你能做什么、你的执行步骤是什么、遇到什么情况怎么处理。

我常用的模板结构是这样:

# 角色 你是XX助手,擅长处理YY任务。 # 工作流程 1. 分析用户请求,判断所需信息; 2. 如果有需要,调用工具获取数据; 3. 基于工具返回的数据生成最终结果。 # 约束 - 只使用工具返回的真实数据,禁止编造; - 如果工具返回为空,如实告知用户; - 保持输出简洁,使用markdown格式; - 不确定的信息不要下结论。 # 输出格式 最终回答需包含:结论、数据来源、操作建议。

这套结构看起来简单,但每一步都在堵漏洞。“禁止编造”是针对模型幻觉的,“工具返回为空要如实告知”是为了防止模型在没数据时脑补一个看似合理的答案。提示词的每一句话都对应一个真实出现过的问题,这是我不断迭代出来的。

我还做过一个对比实验。一个开放式的提示词让智能体自由发挥,结果它经常绕过工具,直接凭“经验”回答用户问题,而且语气自信得像真的查过数据。换成强制工作流之后,准确率明显提升,代价是回答变得模块化。但在工具类任务里,模块化不是缺点,是稳定性的保证。

3.2 工具接入:给智能体装上“手”

工具调用是智能体从“只会说话”到“能做事”的关键。它的原理很直接:模型本身不执行代码,但它能根据工具的描述生成一个结构化调用指令,你的程序负责真正执行,并把结果传回去让模型继续思考。

这里有一个最容易被忽略的重点:**工具描述写得怎么样,直接决定了模型会不会用工具、用得准不准。**模型只能看到工具的name、description和parameters,它根本看不到你的函数实现。所以描述里要写清楚“这个工具是干什么的、在什么场景下用、每个参数是什么格式”。

拿一个查询天气的工具举例,它的JSON Schema长这样:

{ "name": "get_weather", "description": "查询指定城市当日天气。当用户询问天气情况时使用。", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,例如:北京、上海" } }, "required": ["city"] } }

注意description里的“当用户询问天气情况时使用”,这一句是防止模型在别的场景乱调用工具。工具描述越精确,模型的调用准确率越高。我之前有一版天气预报工具没写使用场景,结果用户一句“今天穿什么好”都触发了天气工具,其实模型应该直接给穿搭建议而不是查天气。

执行工具的部分也要仔细处理。每个tool_call事件里都包含了工具名和参数,程序负责分发到对应函数,执行完成后把结果打包成一条role为tool的消息,再放回messages列表。这里要注意,工具结果必须和tool_call_id对应,否则模型会搞不清哪个结果对应哪个调用。

安全方面我的原则只有两条:工具必须白名单化,程序只执行预定义的函数;任何涉及外部网络的调用都要设超时时间,防止智能体卡死。

3.3 记忆机制:上下文窗口不等于记忆库

很多新手把“上下文很长”等同于“记忆很好”,这是个误区。上下文窗口是模型每次调用时能看到的信息总量,它有三个硬限制:有上限、按内容长度计费、太长会稀释注意力。所以记忆管理的本质是:在有限的上下文里,保留最有价值的信息。

我把记忆分成三层处理:

  • 短期对话记忆:保留最近几轮对话原文,超出的部分丢弃或用摘要替代;
  • 工作记忆:当前任务的关键状态和中间结论,单独提炼出来放回提示词;
  • 长期记忆:跨会话的用户偏好、历史事实,存到外面的存储(sqlite、向量库、数据库都可以),需要时主动查询。

这里给一个简单的上下文截断函数:

def trim_messages(messages, max_tokens=8000): """保留system消息和最近的历史,超出部分丢弃""" system_msg = messages[0] tail = messages[-6:] # 只保留最近3轮对话 return [system_msg] + tail

但这只是最粗糙的办法。更好的做法是做摘要压缩:每隔几轮,让模型把之前的对话浓缩成一段摘要,下次调用时把摘要作为上下文放进去。比如“用户希望日报输出按项目分组;已查询Git提交17条;重点是后端模块的代码改动”,这段摘要几十个token,却能保留关键约束和上下文,比塞几千字原文划算得多。

我见过不少项目几个月跑下来,上下文里的历史记录越来越长,到后面每次请求都慢吞吞、贵得离谱,输出质量反而下降。原因就是没有做记忆管理。记忆不是越多越好,知道该忘什么,比知道该记什么更重要。

4. 实操案例:手把手做一个自动日报助手

4.1 需求定义与流程设计

前面讲的都是零件,现在把它们组装起来。我用一个很常见的场景来做完整演示:日报生成助手。它每天自动读取指定项目的Git提交记录和本地待办清单,然后生成一篇结构化日报草稿。

先定义清楚输入输出:

阶段输入输出
用户请求“帮我看看今天的工作内容,生成日报”自然语言请求
工具调用无读取git提交、读取待办文件
最终输出工具返回的原始数据按项目分组的markdown日报

这个需求非常适合智能体:用户请求不确定、需要调用多个工具、最终输出是自然语言生成。而且逻辑足够简单,适合看清全流程。

4.2 分步实现:定义工具到组装循环

第一步,把工具写好。这里用到三个工具:

# tools.py import subprocess from datetime import date def get_git_commits(since: str) -> str: """读取指定日期以来的git提交记录""" result = subprocess.run( ["git", "log", "--since", since, "--pretty=format:%h %s (%an)"], capture_output=True, text=True ) return result.stdout or "无提交记录" def read_todos() -> str: """读取本地TODO清单文件""" try: with open("todos.txt", "r", encoding="utf-8") as f: return f.read() or "无待办事项" except FileNotFoundError: return "未找到待办文件" def get_today() -> str: """获取当前日期,格式YYYY-MM-DD""" return date.today().isoformat()

第二步,把工具描述按照前面说的JSON Schema格式注册到TOOLS列表里。工具参数里要写清楚since参数的含义,因为模型得知道“今天”是从哪里算起的。

第三步是写系统提示词。日报助手的提示词核心是约束输出结构:

# 角色 你是日报生成助手。 # 工作流程 1. 调用get_today获取今天日期; 2. 根据今天日期调用get_git_commits获取提交记录; 3. 调用read_todos获取待办事项; 4. 把信息整合成结构化日报。 # 约束 - 只使用工具返回的真实数据; - 如果某部分没有数据,明确标注“今日无”; - 输出格式为markdown,包含:今日完成、待办事项、明日计划、备注。

第四步就是把主循环跑起来,复用第2节写好的run_agent函数。这里我做了一个小改动:工具描述里要求模型先调get_today,让日期成为一个确定值而不是模型猜出来的。这一步在后面调试里帮了大忙。

4.3 实测结果与踩坑记录

这个案例在实际跑的时候,第一个版本就出问题了。模型回答“今天”的提交记录时,选择了最近自然日的数据,但当时本地仓库的提交时间是昨天。模型把“今天”理解成了“最近一天”,导致日报把昨天的提交也算了进去。

这个问题的根源在于:**模型没有时间感知能力,它不知道now是什么意思。**工具返回的数据里没有显式时间参考,模型只能猜测。解决办法就是我上面加的那个get_today工具,把当前日期作为确定的输入喂给模型,让它基于这个日期来计算since参数。加了之后,日期错误的问题彻底消失了。

第二个踩坑是工具返回内容太长。前几次运行,git提交记录有上千行,直接塞进上下文之后,模型开始“迷失重点”,生成的日报会包含很细节的提交说明,反而忽略了整体结构。后来我在工具函数里做了截断,只保留最近20条提交,超过的部分直接丢弃。这对日报场景完全够用,还能省大量token。

实测下来的成本数据也分享给你:整个流程平均要调用模型3次(取日期、查提交、整合输出),消耗大约5000到7000个token,按主流模型的定价折算,单次成本在几分钱量级。如果你遇到成本明显更高的场景,优先检查是不是上下文塞了太多无关数据。

5. 稳定性比想象力更重要:测试与排查实录

5.1 做一套固定输入的回归测试

智能体项目最容易犯的错误是“调通一次就算完成”。模型的不确定性意味着,你今天调通了,明天同样的输入可能就走了一条完全不同路线,输出也不一样。所以智能体的测试核心不是测功能,而是测稳定性。

我的做法是准备一套固定的测试用例,每个用例包含:用户输入、期望调用的工具、期望的输出格式、期望的关键内容。然后每次改动代码或提示词,就把这套用例跑一遍,记录通过率。

test_cases = [ { "input": "帮我生成今天的日报", "expected_tools": ["get_today", "get_git_commits", "read_todos"], "expected_format": ["## 今日完成", "## 待办事项"], "must_not_contain": ["我不知道", "无法获取"], }, # 更多用例... ]

检查逻辑也很朴实:必须调用指定的工具、必须包含某个字段、不能出现“编造语气”的词。这些断言写成脚本后就可以自动跑。我个人的标准是,任何提示词改动都要让测试通过率达到100%或者接近100%才能上线,否则就是一次没有把握的赌博。

智能体的回归测试还有一个特殊点:要限制测试成本。跑一次完整样例要消耗几百到几千token,我一般只保留20到30个核心用例,覆盖主要流程和边界情况,而不是追求全量覆盖。

5.2 防止失控:重试、兜底与强制退出

模型API不是永远稳定的,再好的提示词也可能遇到模型抽风。生产环境里我会在推理循环外面加三层保护:

  • 重试机制:API调用超时或返回异常时,自动重试三次,每次间隔递增;
  • 格式校验:工具返回非JSON、核心参数缺失时,把错误信息回传给模型让它修正;
  • 强制兜底:达到最大步数或重试仍失败时,返回预设的兜底消息,而不是把错误抛给用户。

这里给一个兜底的例子:

def safe_run_agent(query: str) -> str: for attempt in range(3): try: result = run_agent(query) return result except Exception as e: print(f"第{attempt + 1}次运行失败: {e}") return "当前服务繁忙,暂时无法处理请求,请稍后再试。"

这个兜底消息虽然看起来“不够智能”,但它保证了一个底线:系统不会因为模型异常而出现不可预期的行为。在用户侧,一个稳定的“抱歉”比一段乱七八糟的胡言乱语体面得多。

5.3 给每次运行留一份完整“体检报告”

我把智能体的一次完整运行看作一次需要复盘的过程,而不是一个“输出结果”。每次跑完,我都会保存一份运行记录,内容包含:所有输入消息、每一步的工具调用、每一步的token消耗、最终输出、总耗时。

有了这些记录,你就能回答一些关键问题:为什么这单跑贵了?为什么回答质量下降了?是不是某个工具返回了异常数据?这些问题在正常运行中不会暴露,但一旦出问题,这份运行记录就是唯一的线索。

我现在用的运行记录结构如下:

{ "run_id": "20250117_001", "user_query": "帮我生成今天的日报", "steps": [ {"step": 1, "action": "tool_call", "tool": "get_today", "tokens": 1200}, {"step": 2, "action": "tool_call", "tool": "get_git_commits", "tokens": 1800}, {"step": 3, "action": "tool_call", "tool": "read_todos", "tokens": 900}, {"step": 4, "action": "final_answer", "tokens": 2200} ], "total_tokens": 6100, "duration_ms": 3200 }

这份“体检报告”不只是给自己看的,它也是后续调优的依据。比如某次跑完发现总耗时3.2秒,其中光模型调用就占了大头,你就能据此判断该换更快的模型还是减少调用轮数。

6. 常见问题排查速查表

写了这么多项目,我把最常撞上的问题和排查思路整理成了一张表。适合直接收藏,遇到对应症状就照表排查:

现象可能原因排查方法
智能体反复调用同一个工具,停不下来工具结果没有正确回传给模型;或缺少MAX_STEPS限制检查tool消息是否带上了tool_call_id;确认循环里设了最大步数
工具参数为空或格式错乱JSON Schema写得不严格,描述里没有参数示例在参数描述中补充格式说明和典型值
回答里出现编造的数据工具返回不完整或为空,模型在脑补强制提示词要求“无数据时必须明说”;检查工具返回内容是否为空
生成内容越来越长、速度越来越慢上下文没有裁剪,历史消息无限累积做滑动窗口截断或摘要压缩
模型无端调用不相关工具工具描述没有写适用场景在description里明确“什么情况才用,什么情况别用”
同一输入,今天跑出结果A,明天跑出结果B提示词约束不足,模型自由度太高增加输出格式约束,限定决策路径
成本突然飙高某次工具返回了超大文本在工具侧做结果截断,限制单次返回字数
API偶尔报错,程序直接崩溃没有重试机制和异常捕获包一层重试逻辑,设置兜底返回

这张表看着简单,每一条背后都是我真实踩过的坑。尤其是“工具参数为空”这一条,我做垃圾分类助手的智能体时遇到过,模型调工具时把item参数填成了一个对象而不是字符串,Schema里没写清楚,排查了整整一下午才发现是少了一行参数格式说明。

还有一点经验值得单独说:当问题表现是“模型乱来”时,先不要怀疑模型笨,先怀疑你给模型的信息是不是完整、准确、可判断的。大多数“模型很蠢”的时刻,其实是工具描述、上下文或提示词没有提供足够的决策依据。让模型在确定的信息里做确定的选择,它通常不会让你失望。

最后再分享一点个人体会

做了几个智能体项目之后,我最大的感受是:智能体开发真正的难度不在“AI”部分,而在工程部分。你得把一个模糊问题拆成确定步骤,把模型的不确定性控住,让整个链路在无人盯着的时候也能稳定运行。这套能力不是靠调参调出来的,是靠一次次把提示词改得精确、把工具描述写清楚、把日志存完整积累出来的。

如果只让我给一个建议,那就是:**第一个版本千万别追求复杂功能。**先用假数据把整个链路跑通,再换成真实工具;先做单个工具的调用,再扩展多工具协作;先接受不完美的输出,再做稳定性优化。这条路走通了,智能体项目就成功了一大半。剩下的问题,基本都能在日志和测试里找到答案。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询