☰
AI Agent智能体开发实战:从零搭建工具调用与记忆管理的完整指南
2026/10/8 3:09:47 网站建设 项目流程

简介:一份系统讲解AI Agent智能体开发与应用全流程的PDF教程,面向有一定AI和机器学习基础的研发人员、产品经理及技术爱好者,助力读者从零开始构建属于自己的智能体。教程从人工智能、机器学习、深度学习和大语言模型(LLM)等基础概念切入,厘清AI、AGI、AIGC之间的关系,并重点分析AI Agent与传统程序的区别,展示其在自媒体、智能客服、自动驾驶、股票交易和游戏NPC等领域的应用价值,帮助读者建立从理论到实践的完整认知。随后以字节跳动COZE平台为例,讲解打造Agent的七个步骤:需求梳理、软件选型、提示工程、数据库搭建、UI界面构建、测试评估与部署发布。更包含抖音短视频文案转小红书笔记、小红书文案+OCR+飞书同步两个实战案例,完整演示内容创作和数据处理的做法,并配有操作步骤与代码示例。资源为单个PDF文件,共1个文件,大小12.01MB,已有1356人学习下载,适合希望快速落地Agent开发实战的读者。

1. 从会聊天到能干活:AI Agent 到底改变了什么

很多人第一次用大模型时都会问:“能不能帮我订个会议室?”得到的回复通常是“你可以用某某软件的日历功能,点哪里哪里”——一长串建议,但就是不替你动手。这不是模型笨,而是聊天机器人没有“手”。AI Agent(智能体)的核心变化,就是把模型从“会聊天”升级成“能干活”:模型自己规划步骤、调用工具、观察结果、修正线路,直到任务真正完成。

这篇文章围绕基于 AI Agent 的智能体开发与应用,从一个最小可运行的 Agent 骨架讲起,逐步拆解组件、编排、记忆和上线前必须避开的坑。我不会讲太多玄学,所有内容都能在你自己的笔记本上复现。适合正在做智能体毕业设计的学生,以及想在公司内部快速验证 AI 自动化流程的工程师。如果你想了解智能体到底能不能用、怎么用、坑在哪,这篇可以当你的第一份实战地图。

2. 拆解 Agent 智能体的骨架:模型、记忆、工具与编排

先看一个可工作的智能体由哪些部件组成。很多人觉得 Agent 就是“大模型 + 提示词”,这个理解太粗了。真正跑起来的 Agent,至少包含四块:模型负责决策,记忆负责保存历史,工具负责对外操作,编排负责控制循环顺序。其中模型和工具的分工最容易被忽略:模型不直接执行任何外部动作,它只输出“我建议调用哪个工具、参数是什么”,真正执行的是宿主代码。下面逐个展开。

2.1 核心组件:为什么说工具调用是 Agent 的命门

模型本身是“大脑”,但它没有手。所谓工具调用(function calling),是指模型在生成对话时不是只输出普通文本,而是输出一个结构化的调用请求。举个例子,用户问“北京今天多少度”,如果模型没有工具,它只能凭训练数据里的记忆回答,很可能过期。有了工具之后,模型会输出类似这样的结构:

{ "name": "get_weather", "arguments": "{\"city\": \"北京\"}" }

宿主程序解析这个请求,去真实天气接口拿到数据,再把结果作为一条 tool 角色的消息塞回对话,模型才能基于真实数据给出最终回答。这个“模型提出请求 -> 宿主执行 -> 结果回填”的过程,就是 Agent 和普通聊天机器人的分水岭。

工具调用为什么是命门?因为工具把模型的能力边界从“我脑子里学过什么”扩展到“我能实时获取什么”。没有工具,Agent 只是花哨的文本生成器;有了工具,它才能操作 API、读写文件、发邮件、执行代码。实际项目中,工具数量从几个到几十个不等,但每个工具都必须提供清晰的名称、描述和参数定义。模型选择工具靠的是 description 和参数 schema,而不是靠函数名。所以描述写得太模糊,模型就很容易选错工具,这个在第五章会专门讲。

另外要明白,工具调用并不是模型“会编程”,而是模型学会了从给定的函数清单里选一个并填参数。因此工具清单越短、参数描述越具体,模型选对的概率越高。我给不少项目做优化时,第一步永远是精简工具数量:能合并的工具就合并,能减少的参数就减少,效果立竿见影。

2.2 主流架构选型:ReAct、Plan-and-Execute 与 AutoGPT 式循环

有了工具,还得有一套循环逻辑来驱动模型一步步完成任务。常见的智能体架构有三种,各有不同的决策节奏和控制方式。ReAct 是 Reason + Act 的缩写,每走一步都是“思考 -> 行动 -> 观察”,模型先说要做什么,然后调用工具,看到结果后再继续思考。它适合需要反复和环境交互的场景,比如查资料、整理文件、客服问答。Plan-and-Execute 则先让模型给出一个完整计划,比如“第一步列出文件,第二步创建目录,第三步移动文件”,然后把计划拆成步骤逐个执行。AutoGPT 式循环更像是完全放权:只给一个终极目标,模型自主决定循环多少次、调用哪些工具,直到它认为自己完成了。

三种架构没有绝对的好坏,关键看任务属性。为了更直观,我用一张表把它们对比一下:

架构决策节奏适合场景稳定性上下文开销
ReAct每步推理+行动交互式、需要纠偏的任务较高,出错可在下一步修正较高,每步都要传递历史
Plan-and-Execute先规划再批量执行流程固定、步骤清晰的任务中等,规划出错会连续错较低,执行阶段不用读全部历史
AutoGPT式全自主循环探索型、目标模糊的任务低,容易失控最高,上下文累积极快

从 0 到 1 搭建自己的 Agent,我通常建议从 ReAct 入手。原因很简单:它的每一步决策都能被观察和干预,出了问题容易定位。Plan-and-Execute 适合你已经把任务流程梳理得很清楚时用来省 token。AutoGPT 式看着酷,但缺少硬性边界,稍不注意就会在工具调用里空转,典型的“新手快乐型,老手劝退型”。

这里还要多说一句“编排”的概念。所谓编排,是指代码掌控循环的控制权:什么时候把消息发给模型,什么时候执行工具,什么时候停止。模型不是无限自主运行的,它只是编排循环里的一个决策节点。说得直白一点,爬上还是爬下,是模型决定的,但走几步停,是代码决定的。很多翻车案例都是把控制权全部交给模型,结果模型在循环里出不来。

2.3 用最小代码搭一个可运行的 Agent 骨架

下面用一个兼容 OpenAI Chat Completions 接口的 requests 调用,实现最简 ReAct 骨架。你不需要引入 LangChain 这类重量级框架,因为理解原理比搬框架更重要。先定义工具和执行函数:

import json import requests API_URL = "https://your-endpoint/v1/chat/completions" API_KEY = "your-api-key" # 工具定义:模型会参考这里的描述来决定是否调用 tools = [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的当前天气。用户询问天气、温度、是否适合外出时使用。", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市中文全称,例如 北京、上海", } }, "required": ["city"] } } } ] def exec_tool(name: str, arguments: dict) -> str: """工具执行函数:根据名字和参数做真实调用""" if name == "get_weather": city = arguments.get("city", "") # 真实项目中这里接天气 API,此处用固定返回值做演示 return f"{city},晴,26℃,微风" return f"错误:未注册工具 {name}"

这里的tools列表是传给模型看的函数清单,模型没有能力直接运行,它只是从中选择一个并返回参数。exec_tool是真正的执行者,由你的代码调用。

然后写主循环,它是整个 Agent 的心脏:

def run_agent(user_ask: str, max_steps: int = 5) -> str: """ReAct 主循环:消息发给模型 -> 模型决定调用工具或直接回复""" messages = [ {"role": "system", "content": "你是一个能调用工具的智能体。当用户需要真实信息时,先调用工具,拿到结果后再回答。"}, {"role": "user", "content": user_ask} ] for step in range(max_steps): payload = { "model": "your-model-name", # 换成支持 function calling 的模型 "messages": messages, "tools": tools, "temperature": 0.3, # 低温度让工具选择更稳定 } resp = requests.post(API_URL, json=payload, headers={"Authorization": f"Bearer {API_KEY}"}) resp.raise_for_status() message = resp.json()["choices"][0]["message"] # 模型返回了工具调用请求 if message.get("tool_calls"): messages.append(message) # 把模型的调用请求加入历史 for call in message["tool_calls"]: fn_name = call["function"]["name"] fn_args = json.loads(call["function"]["arguments"]) result = exec_tool(fn_name, fn_args) messages.append({ "role": "tool", "tool_call_id": call["id"], "content": result }) else: # 没有 tool_calls,说明模型已经可以给出最终答案 return message["content"] return "已达到最大步数,任务未完成"

这段代码里有个很容易踩的细节:模型发出工具调用后,必须把这条包含tool_calls的消息加入messages,再追加工具结果。如果漏掉前者,就无法满足接口要求,模型看不到自己刚才请求了什么,后续决策会混乱。

max_steps是硬保险丝,一般设 5~8。temperature建议调到 0.2~0.4,太低模型可能过于机械,太高会增加工具选择的随机性。这个骨架虽然能跑,但离“能用”还很远,后面几章会往里填记忆、加项目结构,再补几个避坑技巧。

3. 从 0 到 1 打造自己的 Agent:需求拆解与项目骨架搭建

有了骨架,下一步是把它做成一个具体的项目。很多人做智能体一上来就想要个“万能助手”,这是最容易失败的做法。Agent 只有在目标明确、工具边界清晰时才可靠。这一章我们用“文件整理智能体”作为实战对象:用户给一个目录路径,智能体自动把文件按类型分类归档,并生成索引。这个例子不需要外部付费 API,完全可以在本地跑通,非常适合作为你第一个 Agent 练手项目。

3.1 定义你的智能体要解决的“一个真实问题”

先别急着写代码,把需求用一句话说清楚:输入一个目录,输出整理后的目录结构和一份 index.json。这个任务看起来简单,但它需要模型做真实决策:遇到.jpg要放进 images,report.pdf要放进 docs,没见过的扩展名怎么办?同名文件会不会覆盖?如果不让模型规划,直接用一套固定 if-else 也能做,但那就不是 Agent 了。Agent 的价值在于面对“模糊分类”时能根据上下文临时决定,甚至向用户提问。

我把需求拆成如下清单:

  • 输入:用户输入的目录绝对路径
  • 输出:目录下文件被分类移动到 images、docs、others 子目录;生成 index.json 记录每个文件的最终位置
  • 工具:list_directory(列出目录内容)、move_file(移动文件)
  • 约束:不能删除任何文件;遇到重名文件自动加后缀;目录不存在时报告错误

为什么要让模型而不是普通脚本来做?因为“分类规则”并不完全固定。比如.md文件到底是文档还是代码的一部分?模型可以根据文件路径或内容做临时判断,或者向用户确认。这种灵活性是脚本难以实现的。

3.2 项目目录结构与配置文件怎么写

一个能长期维护的 Agent 项目,目录结构要清晰。通常我会这样组织:

file_agent/ ├── agent/ │ ├── __init__.py │ ├── core.py # ReAct 主循环 │ ├── tools.py # 文件操作工具注册 │ ├── config.py # 读取环境变量和配置 │ └── memory.py # 记忆模块,第4章会用到 ├── config.yaml # 模型与运行参数 ├── requirements.txt └── run.py # 入口脚本

config.yaml里放所有可调参数,方便实验时不停改配置而不动代码:

model: your-model-name api_base: https://your-endpoint/v1 api_key_env: AGENT_API_KEY # 从环境变量取,别明文 temperature: 0.2 max_steps: 6 timeout_seconds: 20

几个参数的含义:model一定要选支持 function calling 的模型,否则你传tools过去也可能被忽略。api_base是兼容 OpenAI Chat Completions 接口的地址,本地部署的 vLLM、其他云厂商都行。temperature控制决策随机性,Agent 场景建议 0.2 左右。max_steps是每次任务能循环的最大步数,太小了复杂任务完不成,太大了死循环风险高。timeout_seconds是 HTTP 超时,避免某个工具或接口卡死把整个 Agent 拖住。

在config.py里,用环境变量加载密钥,避免把密钥写进配置文件:

import os import yaml def load_config(): with open("config.yaml", encoding="utf-8") as f: cfg = yaml.safe_load(f) cfg["api_key"] = os.environ.get(cfg["api_key_env"], "") return cfg

顺带说一句,requirements.txt里只需要requests和pyyaml,这是刻意保持轻量。很多智能体项目一上来就装 LangChain、LangGraph、Chroma,最后环境一团乱麻。从 0 开发智能体,核心循环用几十行代码就能实现,依赖越少,排查问题越容易。

3.3 第一步跑通:带工具调用的最小主循环

现在把工具定义和主循环换成文件整理版本。tools.py里注册两个工具,并绑定了实际执行函数:

import os import json from pathlib import Path tool_registry = [] # 工具注册表,一个 schema 对应一个执行函数 def register(schema): """装饰器:把 schema 和执行函数绑定并加入注册表""" def decorator(func): schema["function"]["_func"] = func tool_registry.append(schema) return func return decorator @register({ "type": "function", "function": { "name": "list_directory", "description": "列出指定目录下所有文件和子目录的名字。整理文件前必须先调用它查看目录内容。", "parameters": { "type": "object", "properties": { "path": {"type": "string", "description": "要列出的目录绝对路径,例如 /home/user/downloads"} }, "required": ["path"] } } }) def list_directory(path: str) -> str: try: items = os.listdir(path) return json.dumps(items, ensure_ascii=False) except Exception as e: return f"读取失败:{e}" @register({ "type": "function", "function": { "name": "move_file", "description": "把文件从源路径移动到指定目录。只用于移动普通文件,不用于移动目录。", "parameters": { "type": "object", "properties": { "source": {"type": "string", "description": "要移动文件的完整路径"}, "target_dir": {"type": "string", "description": "目标目录完整路径,会自动创建"} }, "required": ["source", "target_dir"] } } }) def move_file(source: str, target_dir: str) -> str: try: os.makedirs(target_dir, exist_ok=True) dest = str(Path(target_dir) / Path(source).name) if os.path.exists(dest): # 重名文件加 _1 后缀,避免覆盖 base, ext = os.path.splitext(dest) dest = base + "_1" + ext os.rename(source, dest) return f"已移动到 {dest}" except Exception as e: return f"移动失败:{e}"

这段代码里用了装饰器,目的就是让工具定义和执行函数成对出现,避免在多个文件里手工维护名字和函数映射。move_file里对重名文件的处理是一种被动兜底,因为模型不一定每次都能预料到冲突。

core.py的主循环基本复用第 2 章的骨架,只是把tools换成从tool_registry提取的 schemas,并调用注册表里对应的函数:

import json import requests from tools import tool_registry def call_tool(name, args): for item in tool_registry: if item["function"]["name"] == name: return item["function"]["_func"](**args) return "错误:未注册工具" def run_agent(user_ask): messages = [ {"role": "system", "content": "你是文件整理助手。你只能使用提供的工具操作文件系统。每次整理完成后给出汇总。"}, {"role": "user", "content": user_ask} ] for _ in range(6): schemas = [] for t in tool_registry: func = t["function"] schemas.append({"type": "function", "function": {k: v for k, v in func.items() if k != "_func"}}) payload = { "model": "your-model-name", "messages": messages, "tools": schemas, "temperature": 0.2, } resp = requests.post(API_URL, json=payload, headers={"Authorization": f"Bearer {API_KEY}"}) message = resp.json()["choices"][0]["message"] messages.append(message) if message.get("tool_calls"): for call in message["tool_calls"]: args = json.loads(call["function"]["arguments"]) result = call_tool(call["function"]["name"], args) messages.append({"role": "tool", "tool_call_id": call["id"], "content": result}) print(f"[步骤] {call['function']['name']}({args}) -> {result}") else: return message["content"] return "达到最大步数,未完成"

运行入口run.py只要几行:

from agent.core import run_agent if __name__ == "__main__": path = input("请输入要整理的目录路径:") answer = run_agent(f"请整理目录 {path},将文件按类型分类移动到 images、docs、others 子目录") print(answer)

这一步跑通的关键在于模型能不能按顺序调用list_directory然后再调用move_file。如果你发现模型一次只移动一个文件就停了,先检查max_steps够不够,再检查system提示词里是否明确说了“持续处理直到所有文件都被移动”。这算是 Agent 开发里第一个典型的 prompt 调优点。

4. 让 Agent 变可靠:记忆管理、多轮对话与上下文裁剪

没有记忆的 Agent 像金鱼:你和它聊完一轮,下一轮它就不记得了。更麻烦的是,即使在同一轮任务里,随着调用工具次数增加,历史消息越来越长,模型开始顾此失彼。这一章讲清楚记忆是什么、在哪一层解决,并给出几个能直接用的内存管理方案。

4.1 三种记忆:短期上下文、长期向量记忆、工作记忆

在智能体系统里,“记忆”不是单个组件,而是分三层的。短期上下文就是messages列表里所有历史消息,模型每一轮都能看到它,这是最直接也最占资源的记忆。工作记忆是 Agent 在执行过程中维护的状态,比如“已经移动了 3 个文件”“当前目录是 /home/user/downloads”,一般用程序里的变量存着,不消耗模型 token,但需要开发者在工具调用之间显式传递。长期记忆跨会话保存,比如用户上次说“我喜欢按年份归档图片”,存到本地存储或向量库里,下次任务开始时检索出来。

为什么要分这么多层?因为上下文窗口是稀缺资源,而工作记忆和长期记忆能帮我们“倒掉”一部分短期上下文。一个常见场景:Agent 连续处理了 50 个文件,每个文件的移动结果都作为 tool 消息留在 messages 里。到第 51 个文件时,模型已经很难记住用户最初的需求,甚至会被中间结果带偏。这时如果有一个工作记忆变量记录“已完成 50 个”,并在每轮开始前把计数写进 system 消息,模型就不会迷路。

实践上,我给 Agent 做的第一件事就是在run_agent外面包一个状态对象,把用户原始目标、当前目录、已处理文件列表都放进去。对话消息只保留最近几步,关键状态用变量单独维护。这个习惯能显著提升多步任务的完成率。

4.2 上下文窗口不够用怎么办:裁剪、摘要与结构化

上下文管理没有万能钥匙,通常三种策略混着用。

裁剪最直接:只保留 system 消息和最近 N 条消息,中间的旧对话直接丢。适合那些早期消息已经不影响最终结果的场景。代码很简单:

def trim_messages(messages, keep_last=10): system_msgs = [m for m in messages if m["role"] == "system"] tail = messages[-keep_last:] return system_msgs + tail

但裁剪会丢事实。比如用户第一轮说了“我是市场部的小王”,后面几轮想让他再确认身份,如果被裁掉就麻烦了。所以更稳妥的是摘要。用一个低成本的模型调用,把将要被裁剪的旧消息压缩成一段话:

def summarize(messages): text = json.dumps(messages, ensure_ascii=False) prompt = f"把下面的对话压缩成一段100字以内的摘要,保留所有关键事实和用户要求:\n{text}" resp = requests.post(API_URL, json={ "model": "summary-model", "messages": [{"role": "user", "content": prompt}] }) return resp.json()["choices"][0]["message"]["content"]

然后把摘要作为一条新的 system 消息放在 messages 最前面,再丢掉被摘要的旧消息。摘要的代价是多了一次模型调用,但换来了关键信息不丢失。

更高级的是结构化记忆:把对话里的“用户偏好”“任务状态”抽取成 JSON。

{ "user": "小王", "department": "市场部", "processed_files": 50, "preferred_category": "year" }

模型不再需要回顾几十轮原始对话,直接读这个 JSON 就够。结构化记忆最省 token,但对抽取质量要求高,适合消息量大且字段明确的任务。我一般会在 Agent 里同时用这三种:短期消息裁剪、中层摘要、关键状态走结构化文件。

4.3 加一个简单的向量记忆模块

长期记忆部分,我们用一个不依赖外部数据库的轻量实现。下面这个SimpleMemory类基于关键词重合度打分,把历史记录按相关程度检索出来,插入当前上下文。它比真正的向量数据库粗糙,但胜在零依赖、能跑通。

import json import re from pathlib import Path class SimpleMemory: def __init__(self, storage_path: str = "memory.json"): self.path = storage_path self.data = [] if Path(storage_path).exists(): self.data = json.loads(Path(storage_path).read_text(encoding="utf-8")) def add(self, record: dict): self.data.append(record) Path(self.path).write_text( json.dumps(self.data, ensure_ascii=False, indent=2), encoding="utf-8" ) def search(self, query: str, top_k: int = 3) -> str: """按关键词重合度检索,返回最相关的记忆内容""" query_tokens = set(re.findall(r"[\u4e00-\u9fff\w]+", query)) scored = [] for idx, record in enumerate(self.data): text = record.get("content", "") + record.get("key", "") tokens = set(re.findall(r"[\u4e00-\u9fff\w]+", text)) if not tokens: continue score = len(query_tokens & tokens) / max(1, len(query_tokens | tokens)) scored.append((score, idx)) scored.sort(reverse=True, key=lambda x: x[0]) hits = scored[:top_k] relevant = [self.data[idx]["content"] for score, idx in hits if score > 0.15] return "\n".join(relevant)

用法是在run_agent开始时,先用用户输入检索记忆,把命中内容以“已知的历史信息”形式拼入 system 消息:

memory = SimpleMemory("memory.json") history = memory.search("整理图片") history_prompt = f"以下是你对用户的历史了解,供参考:\n{history}" if history else ""

top_k控制检索条数,内容太多反而会干扰模型。阈值 0.15 是经验值,调太高可能搜不到,调太低会带出无关内容。真正的生产项目会用 embedding 模型把文本转成向量,再用 SQLite-vec 或 FAISS 做相似度检索,但原理和这里的search是一样的:挑选相关片段,塞进上下文,而不是把所有历史都堆给模型。

这里有个容易被忽略的坑:记忆检索也不能“无限喂”。如果每次对话都插入一大堆历史记录,上下文还是会爆。所以检索到的内容要尽量短,一条记忆最多一两句话。宁可少而准,不要多而杂。

5. Agent 实战落地避坑指南:5 个让新手翻车的典型问题

这一章写的都是我在实际开发智能体时踩过的坑,有些是真刀真枪跑线上时翻车的血泪经验。每一条按“现象 -> 原因 -> 解决”的顺序展开,你可以直接对号入座。

5.1 现象:智能体陷入“工具调用死循环”停不下来

日志里模型一直在调用同一个工具,比如反复查询某个目录内容,但永远不给出最终答案;token 浪费以肉眼可见的速度增长。这就是典型的 Agent 失控。原因首先是没有给循环设置硬上限,模型可以在 for 循环里无限跑。其次是模型把工具返回结果当成了新的任务指令,总觉得“还需要再看一次”。还有一个隐性原因:工具返回内容里包含了无关字段,比如查询天气返回了大段湿度、空气质量指数,模型误以为还没拿到关键信息。

解决方式分三层。第一层,在系统提示词里写死约束:“当你已获得足够信息回答用户时,必须直接给出最终答案,不要再次调用工具。”第二层,在工具返回内容前增加完成标志,比如“查询完成,北京:晴,26℃”。第三层,在主循环里检查连续相同调用:

last_tool = None for step in range(max_steps): # ... 发请求,拿 message if message.get("tool_calls"): name = message["tool_calls"][0]["function"]["name"] if name == last_tool: # 连续两次调用同一个工具,很可能是死循环,强制输出当前结果 return message["content"] last_tool = name # ... 其余逻辑

这个保险丝不保证每次都完美,但能阻止最傻的死循环。设置max_steps=6的经验值是 5~8,太低复杂任务完不成,太高容易烧钱。

5.2 现象:模型选错工具或把参数填错

用户说“整理一下图片”,模型却调用了delete_file;或者把路径参数填成了相对路径images,工具执行时根本找不到目录。这类问题在工具数量超过 5 个之后非常常见。根本原因是工具描述和参数 schema 写得不够“面向模型”。模型不是在理解你的函数代码,它只读到一段文字,这段文字的质量直接决定选择的正确性。

解决时我一般会把工具描述重写为“触发条件”句式。比如delete_file的 description 改成“仅在用户明确要求删除文件时才能调用,整理、移动、分类场景一律禁用”。参数描述里加示例值和绝对路径要求:

"path": { "type": "string", "description": "文件绝对路径,示例:/home/user/downloads/photo.jpg" }

另外,工具数量要克制。能用 5 个工具解决的场景就不要挂 15 个。每加一个工具,模型选错的可能性就高一分。如果模型本身对 function calling 支持不好(比如某些轻量模型),可以退一步,让模型直接输出 JSON 字符串,由你写解析器。但那样稳定性会下降,建议还是选支持 function calling 的模型。

5.3 现象:多轮对话后上下文爆炸,精度断崖下跌

对话进行到七八轮,Agent 开始答非所问,甚至复读工具返回的原始 JSON;有时还没到第八轮就报“超出最大 token”。原因很清楚:messages数组无限制累积,每轮工具返回的几 KB 内容都被原样发给模型。而大多数模型对长上下文的注意力是有限的,特别是中间段的历史很容易被忽略。

我采取的方案是“主动管理上下文”,而不是等它爆了再截断。每轮工具结果返回后,立刻精简成一个短摘要再写入 messages,比如“已移动 image_001.jpg 到 images/”;原来几十行的文件列表,只保留“目录下包含 5 个图片,4 个文档”。同时用上一章的trim_messages控制总消息条数。另一个技巧是给工具执行函数加一个result_summary返回值,代替原始输出,从源头减少数据量。

# 工具返回前先精简 raw_items = os.listdir(path) summary = f"目录 {path} 下共有 {len(raw_items)} 项,文件名列表:{raw_items[:5]}..." return summary

上线前,我还会写一个小脚本模拟多轮对话,观察每轮 messages 的 token 数是否符合预期。如果一轮能涨一两千 token,那撑不了几轮,必须提前压缩。

5.4 现象:装完一堆库之后,环境冲突到想放弃

pip install langchain langgraph openai faiss-cpu 之后,一条条依赖冲突弹出来:A 要 numpy<2.0,B 要 numpy>=1.26,最后连 import 都报错。这是很多智能体项目折在半路的真实原因。框架本身没有错,但这些库迭代太快,版本之间 API 差异极大,跟着线上教程装最新版,很容易把环境搞成一锅粥。

如果项目目标只是跑通一个最小 Agent,我的建议是彻底抛弃重框架,只用requests和标准库。前面几章的代码已经证明,几十行就能实现核心能力。如果你确实需要 LangGraph 这类框架的图和状态管理,那就用 Python 虚拟环境,并在requirements.txt里锁定精确版本:

langgraph==0.0.49 langchain==0.2.7 openai==1.30.1

注意版本号要与你实际使用的框架 API 对齐。升级前先看官方迁移文档,不要无脑升最新。排查环境问题时,先pip freeze看她实际装了什么,再对照异常信息,而不是反复卸载重装。

5.5 现象:Agent 在沙箱外乱执行危险命令

我给一个内部工具加过“运行 shell 命令”的权限,初衷是让 Agent 能执行ls和cat查看环境。结果某个测试用例里,模型为了“清理临时文件”自己生成了rm -rf /home/xxx/tmp,差点把整个测试目录删掉。这个事故让我意识到一个铁律:永远不要让模型直接接触完整 shell,因为你无法预测它下一步生成什么命令。

正确做法是把 shell 权限封装成白名单工具。比如只提供一个run_safe_command,函数内校验命令是否在允许列表里,不允许的一律拒绝:

ALLOWED = {"ls", "cat", "pwd"} def run_safe_command(command): if command.split()[0] not in ALLOWED: return f"禁止执行命令:{command}" # 真实执行逻辑用 subprocess,设置 timeout

对于删除、移动、写入这类高影响操作,工具签里要加一个confirmed参数,并强制要求人工确认。在开发阶段,甚至可以默认只让 Agent “输出计划”,不实际执行,等人工点确认再跑。安全边界怎么强调都不过分,给 Agent 的工具权限必须遵守最小权限原则,和对待外部用户输入一样。

6. 把 Agent 从“能跑”推到“能用”:测试、评估与上生产线的三个验证技巧

一个能在本地跑通一两次的 Agent,和能稳定上线的 Agent,中间隔着非常多的测试与验证工作。这里分享三个我一直在用的技巧,能帮你避掉大部分线上翻车。

第一个技巧是建立回归测试集。准备 10 到 30 条固定的输入样例,每一条都标注期望结果。比如“整理目录 A,最终必须生成 index.json,且不能删除任何文件”。每次修改提示词、工具定义或主循环代码后,跑一遍测试集,统计通过率。不要只测功能,还要测异常场景:目录不存在、工具调用超时、模型返回非法 JSON。这个测试集是我所有 Agent 项目的第一道防线,没有它,你永远不知道哪次改动把旧功能弄坏了。

第二个技巧是记录完整的工具调用轨迹。在run_agent里加一个log列表,把每一步的 step 序号、工具名、参数、返回结果都存下来,任务结束后写入 JSON 文件。线上排查时,轨迹就是 Agent 的黑匣子,能直接看到它为什么走到哪一步。检查轨迹时我特别关注两点:有没有冗余调用,有没有危险操作。比如明明一次就能查完的数据,模型却调了三次,说明提示词要优化。

log = [] # 每执行一个工具后: log.append({ "step": step, "tool": name, "arguments": args, "result": result }) # 结束时保存 Path("trace.json").write_text(json.dumps(log, ensure_ascii=False, indent=2), encoding="utf-8")

第三个技巧是用低成本模型做回归筛选。不要每次都拿最贵的大模型跑全量测试集,先用便宜小模型快速扫一遍,剔除明显的错误案例,只剩下“小模型有分歧”的样例再给大模型判断。这样能省下大笔 token,同时依然能捕捉到大多数回归问题。我现在每个 Agent 项目都会配两套模型:一套负责开发期快速迭代,一套负责正式推理。

这三个技巧里,我踩过最大的坑是跳过测试集直接上线。结果线上用户随意输入一句话,Agent 就按字面意思删掉了一个重要目录。从那以后我把“危险操作审计”写进了流程,先用轨迹 log 跑模拟任务,再人工审核,最后才放真实用户。验证不是可有可无的步骤,而是让 AI Agent 能从玩具变成工具的最后一道护栏。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询