☰
从零搭建AI智能体:Agent架构核心与工具调用实战
2026/10/22 7:58:26 网站建设 项目流程

做AI应用落地这两年,我越来越觉得很多人把AI智能体的“架构”想复杂了。一聊起来就是多智能体协同、复杂规划、长期记忆、强化学习微调……听起来确实高级,但真正动手写代码的时候,大多数人第一步就卡住了,根本不知道从哪里把第一行代码敲下去。这篇文章我不讲那些玄乎的概念,直接带你把一个可运行的AI智能体从零搭出来,核心代码全部贴出来,每一个设计点都解释清楚为什么这么做。如果你正在入门AI应用开发,或者已经在业务里接了大模型接口却总觉得“差一层”,那你来对地方了。

这个最小闭环跑通之后,你会发现所谓Agent架构并不神秘,本质就三件事:让模型看见可用工具、按循环执行工具调用、把结果沉淀回对话里继续推理。后面再谈记忆、规划、并发、安全,都是在往这个核心闭环上做增强。我会用Python做示例,尽量避开某个特定厂商的SDK,让你能平滑迁移到自己服务的模型接口上。先把架构骨架看清楚,再谈其他。

1. 先理解AI智能体的架构骨架

1.1 从“能对话”到“能干活”,Agent解决的本质问题

过去一年的变化很明显:单纯聊天问答的应用已经越来越没有竞争力了。你问一个大语言模型“帮我查一下项目目录下有哪些文档”,它只会跟你道歉,因为它压根碰不到你的文件系统。但AI智能体可以做到,本质差异在于它多了一层“干活”的能力,也就是工具调用。

你可以把大模型理解成一个知识渊博但被困在封闭房间里的顾问。他什么都知道一点,但就是没有手没有脚,没办法翻资料、查系统、发请求。AI智能体要做的,就是给这个顾问配上一个传话筒、一双手,让他可以通过一系列预设好的工具去收集信息、执行操作,再把结果拿回来整理成答复。这个比喻基本就是Agent架构的核心逻辑:模型负责推理和决策,工具负责落地和交互。

所以我在设计Agent时,第一个想清楚的问题永远是:模型这次到底要解决什么问题,需要碰哪些外部资源。资源抽象成工具,问题拆成多步任务,模型在循环里反复“想一步、动一步”,最终收敛到结果。有了这个视角,再去看各种Agent框架,你就不会迷路。

1.2 一个Agent需要的核心模块长什么样

一个能跑通任务的Agent,至少包含几个模块。不一定要很重,但职责必须清晰:

  • 任务理解模块:把用户输入转成结构化目标,弄清楚要做什么、有哪些约束;
  • 工具注册表:维护所有可调用函数及其参数说明,让模型知道“我有哪些牌可以打”;
  • 推理与决策模块:本质是模型本身,负责分析当前状态、选择下一步动作;
  • 执行器:根据模型选择的工具名和参数,去真实调用对应函数;
  • 记忆模块:分短期和长期,短期处理当前多轮上下文的裁剪,长期沉淀历史经验和事实数据;
  • 校验与修正模块:发现工具调用失败或结果不合理时,反馈给模型纠正。

很多文章会把Agent描述得很玄,实际拆开就是这些部件。我给你打个比方:工具注册表有点像餐厅菜单,模型是顾客,菜单越清晰顾客越容易点对菜;执行器是后厨,菜单上有的菜才做得出来;记忆是服务员记忆力,既不能把前几桌的菜搞混,也不能忘了客人刚说过忌口。

模块之间的关系比模块本身更重要。模型并不是直接调用函数,而是输出一个“想调用哪个工具、参数是什么”的结构化描述,Agent代码再把它翻译成真实函数调用。这个解耦设计是Agent架构里最关键的一步。

1.3 最容易被误解的三个架构观念

我见过不少团队在Agent架构上栽跟头,往往不是因为代码难写,而是三个观念没掰正。

第一个错误观念是“Agent就是调大模型接口”。实际上,调接口只是一次对话,而Agent是一整个循环。模型回一句、Agent执行一步工具、把新的观察回填给模型、模型再推理下一步。这个循环的次数和终止条件,才是架构里要重点设计的。

第二个错误观念是“工具越多越强大”。工具定义如果混乱、描述含糊、参数文档不完整,模型反而更容易选错或手足无措。我自己的经验是,工具注册表宁可精简,每个工具描述里写清楚“什么时候用”“参数代表什么”,比堆二十个模棱两可的工具好用得多。

第三个错误观念是“提示词能解决一切”。提示词确实重要,但工程上必须有兜底方案:工具可能抛异常、模型可能连续调用同一个工具死循环、上下文可能膨胀到溢出。这些都要靠代码机制去约束,不是多写两句“请小心谨慎”就能解决的。

2. 手把手搭建最小可用Agent:代码与运行流程

2.1 准备环境与基础依赖

这个项目对Python版本要求不高,3.9以上完全够用。整个Agent核心其实只需要一个第三方库,那就是requests,用来向模型服务端发起HTTP请求。其他标准库如json、os、time足够。

我建议先建一个独立目录,再用虚拟环境隔离依赖,避免把系统Python环境搞乱。命令行操作如下:

mkdir agent-demo && cd agent-demo python3 -m venv .venv source .venv/bin/activate pip install requests

接着设置两个环境变量,一个指向模型服务的接口地址,一个放密钥。我特意把接口地址做成可配置而不是写死,就是为了让你能对接自己实际在用的模型服务:

export LLM_API_ENDPOINT="https://your-llm-server.example.com/v1/chat/completions" export LLM_API_KEY="your-key-here"

现在的模型服务商基本都兼容同一种工具调用格式,也就是模型会返回一个结构化结果,告诉你它想调用哪个工具、参数是什么。我用HTTP请求的方式直接对接这个通用格式,不需要依赖某个厂商的专用SDK,这样后续换模型服务商成本会低很多。

2.2 工具注册表:让大模型“看见”可用的函数

工具注册表这个模块,我的设计逻辑很简单:一个字典,存放工具名、描述、参数格式说明和真实处理函数。每次和模型通信之前,Agent会把所有工具的描述信息序列化成一个JSON结构发给模型,等同于给模型一张完整的菜单。

from typing import Callable, Any class ToolRegistry: def __init__(self): self._tools = {} def register(self, name: str, description: str, schema: dict, handler: Callable): self._tools[name] = { "description": description, "schema": schema, "handler": handler, } def specs(self): tools = [] for name, info in self._tools.items(): tools.append({ "type": "function", "function": { "name": name, "description": info["description"], "parameters": info["schema"], }, }) return tools def run(self, name: str, **kwargs): if name not in self._tools: raise KeyError(f"工具 {name} 未注册") return self._tools[name]["handler"](**kwargs)

这里参数格式schema用的是JSON Schema结构。为什么要这个结构?因为模型的工具调用结果需要根据它生成正确的参数。比如你告诉模型某个工具需要“query”字段,模型就会在返回内容里带上{"query": "监控告警"}。如果参数说明不清晰,模型会乱传参数,所以注册表里每一个工具的description和schema都要认真写。

为了方便给函数打标注,我再加一个装饰器工厂,让工具定义看起来更干净:

def register_tool(name: str, description: str, schema: dict): def decorator(func): registry.register(name, description, schema, func) return func return decorator

现在定义两个模拟的本地知识库工具,先不接真实数据库,但整个调用链路是通的:

FAKE_DOCS = { "监控告警设计": "核心建议:告警分级、自动通知、关联分析", "容量规划方案": "核心建议:月度评估、预留20%冗余、关键路径降级", "发布流程规范": "核心建议:灰度发布、回滚预案、变更审批", "日志采集治理": "核心建议:统一格式、采样降噪、冷热分层存储", "故障应急预案": "核心建议:定期演练、明确角色、告警与处置联动", } @register_tool( "search_docs", "在本地知识库中检索与query关键词相关的文档,返回文件名和摘要", { "type": "object", "properties": { "query": {"type": "string", "description": "检索关键词"} }, "required": ["query"], } ) def search_docs(query: str): result = {} for title, summary in FAKE_DOCS.items(): if query in title or query in summary: result[title] = summary return result if result else {"提示": "没有匹配的文档"} @register_tool( "list_docs", "列出本地知识库中的所有文档名称", {"type": "object", "properties": {}} ) def list_docs(): return list(FAKE_DOCS.keys())

工具定义里有一点要注意:description一定要写清楚“什么场景下用”。因为模型是根据文字描述去匹配用户意图的,描述越具体,选错工具的概率越低。比如这里我写“检索与query关键词相关”,模型遇到“找一找监控告警相关文档”这类请求时,就会优先想到search_docs而不是list_docs。

2.3 Agent主循环:ReAct推理与工具调用的完整实现

核心还没登场,Agent主循环。先讲思路。ReAct模式,通俗说就是“想一步,做一步”。模型先思考当前要用什么工具、传什么参数,执行完之后把结果塞回对话里,模型再根据结果决定是继续调用工具还是给最终回答。这个循环我给它设了个最大步数上限,避免模型卡在死循环里。

先实现一个模型客户端,封装接口调用逻辑:

import json import os import time import requests class LLMClient: def __init__(self, endpoint=None, api_key=None, model=None): self.endpoint = endpoint or os.getenv("LLM_API_ENDPOINT") self.api_key = api_key or os.getenv("LLM_API_KEY") self.model = model or os.getenv("LLM_MODEL", "default-model") self.timeout = int(os.getenv("LLM_TIMEOUT", "60")) def chat(self, messages, tools): headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", } payload = { "model": self.model, "messages": messages, "temperature": 0.2, } if tools: payload["tools"] = tools resp = requests.post(self.endpoint, headers=headers, json=payload, timeout=self.timeout) resp.raise_for_status() data = resp.json() return data["choices"][0]

这里有个细节:temperature我设成了0.2。工具调用任务追求的是确定性和准确性,温度太高会导致模型偶尔输出很随机的参数,同一个任务两次结果不一致。如果后面你要用Agent做创意类任务,可以适当调高,但工具调用的主流程建议保持低温度。

然后是Agent本体:

class Agent: def __init__(self, client: LLMClient, registry: ToolRegistry, system_prompt: str, max_steps: int = 6): self.client = client self.registry = registry self.system_prompt = system_prompt self.max_steps = max_steps def run(self, user_input: str): messages = [ {"role": "system", "content": self.system_prompt}, {"role": "user", "content": user_input}, ] for step in range(self.max_steps): try: result = self.client.chat(messages, self.registry.specs()) except Exception as e: return f"模型调用失败: {e}" message = result["message"] messages.append(message) reason = result.get("finish_reason") if reason == "stop" or not message.get("tool_calls"): return message["content"] or "任务执行结束" for call in message["tool_calls"]: fn = call["function"] name = fn["name"] args = json.loads(fn["arguments"] or "{}") try: tool_output = self.registry.run(name, **args) except Exception as e: tool_output = {"error": str(e)} messages.append({ "role": "tool", "tool_call_id": call["id"], "content": json.dumps(tool_output, ensure_ascii=False), }) return "达到最大步数,任务未完成"

这个循环的逻辑可以这样理解:messages是Agent的完整工作记忆,模型每一步都基于这份记忆做决策。模型想调用工具,我就去执行;工具结果作为一条role为tool的消息追加进来;模型再读一遍,直到它觉得信息够了,给出一段最终答复,此时finish_reason通常是stop。这就是完整的ReAct循环。

还要配一个系统提示词。系统提示词其实就是给这个Agent设定“职业身份”和操作约束:

SYSTEM_PROMPT = """你是某本地知识库助手。 你需要根据用户请求,选择合适的工具检索资料。工具返回结果后,请整理成简洁、准确的中文回答。 如果工具没有返回任何内容,请直接告知用户未找到相关资料。 不要编造工具结果,一切结论以工具返回为准。 """

这里那句“不要编造工具结果”不是摆设。我在实际使用中遇到过不少模型在没有工具返回的时候强行给一个看似合理的回答,这非常危险,尤其是在知识库类任务里。所以系统提示词里把这条约束写清楚,虽然不能百分百杜绝,但能明显降低幻觉概率。

2.4 跑通一个真实任务

现在把整个流程串起来跑一次。假设用户提问“搜索与监控告警有关的文档,并总结核心建议”,你可以把下面的代码作为入口文件:

if __name__ == "__main__": client = LLMClient() agent = Agent(client, registry, SYSTEM_PROMPT) answer = agent.run("搜索与监控告警有关的文档,并总结核心建议") print(answer)

整个执行流程在内部是这样的:

第一步,模型看到用户问题,结合工具清单,规划出动作:search_docs,参数query设为“监控告警”。于是返回一个tool_calls结构。第二步,Agent执行search_docs,找到“监控告警设计”这篇文档的摘要。第三步,模型读到工具结果后,整合成最终回答:“监控告警设计的核心建议是告警分级、自动通知、关联分析”。整个流程大概在几秒内完成。

我建议你拿到代码后先跑一遍,然后再去改数据、改工具、改系统提示词,看看同一个任务在不同参数组合下会有哪些变化。我在调试时最喜欢的做法是打印出每一轮messages内容,这样能直观看到模型决策的过程,比猜问题根源高效得多。

3. 让Agent更聪明:记忆、规划与自我修正的工程实现

基础Agent已经能跑通单轮工具调用任务,但离“好用”还有距离。真实场景往往是多轮对话、长文本检索、多步骤拆解,要处理这些,就必须给Agent加上记忆管理和任务规划能力。

3.1 短期记忆管理:消息窗口的裁剪与摘要

短期记忆是什么?其实就是当前对话里那串messages数组。问题是,每调用一次工具,就会增加至少两条消息(模型决策+工具结果),几十步下来messages会非常长,既占token又可能超出模型上下文窗口。所以必须有裁剪策略。

我常用的策略是滑动窗口加摘要压缩:保留系统提示词和最近N轮对话,把更早的内容浓缩成一小段摘要,拼在窗口前面。这样既不会完全丢上下文,也能控制长度。

class SlidingMemory: def __init__(self, keep_rounds=10): self.keep_rounds = keep_rounds def compress(self, messages): if len(messages) <= self.keep_rounds * 2 + 1: return messages head = messages[:1] rest = messages[1:] recent = rest[-self.keep_rounds * 2:] dropped = rest[:-self.keep_rounds * 2] summary = self._summarize(dropped) return head + [{"role": "user", "content": f"之前对话摘要:{summary}"}] + recent def _summarize(self, dropped): parts = [] for msg in dropped: content = msg.get("content", "") if isinstance(content, str) and content: parts.append(content[:80]) elif msg.get("tool_calls"): for call in msg["tool_calls"]: params = call["function"]["arguments"] parts.append(f"调用工具{call['function']['name']}({params[:50]})") return ";".join(parts[-20:]) if parts else "无关键历史信息"

这个实现是轻量级的,实际生产里我更推荐用模型做摘要,比如“请把以上内容压缩成50字以内的要点”,但代价是额外一次模型调用。所以压缩频率要控制好,不必每一轮都做,只有当消息数超过阈值时才触发。

还有一个容易被忽略的点:tools本身也会占上下文,工具描述越长、数量越多,留给对话的空间就越少。所以注册表的描述要精炼,schema里的properties只放必要字段,一些啰嗦的说明可以挪到description里。

3.2 长期记忆:把关键信息沉淀到可检索的存储中

长期记忆解决的是另一个问题:这次任务学到的知识,下次还能不能复用。比如用户上次明确说“告警阈值不要写死”,如果Agent没有长期记忆,下次生成方案时还是老样子,这就很可惜。

我设计了一个极简版本的长期记忆存储,先不做重向量库,但保证接口语义清晰,以后要换成向量检索时改动很小。核心思路就是把一段文本保存下来,再按关键词相关性召回。

class LongTermMemory: def __init__(self, storage_path="memory.json"): self.path = storage_path self.items = [] self._load() def _load(self): if os.path.exists(self.path): with open(self.path, "r", encoding="utf-8") as f: self.items = json.load(f) def add(self, text, meta=None): self.items.append({"text": text, "meta": meta, "time": time.time()}) self._flush() def _flush(self): with open(self.path, "w", encoding="utf-8") as f: json.dump(self.items, f, ensure_ascii=False, indent=2) def ngrams(self, text, n=2): text = text.replace(" ", "") return {text[i:i+n] for i in range(len(text) - n + 1)} def search(self, query, top_k=3): q_grams = self.ngrams(query) scored = [] for idx, item in enumerate(self.items): doc_grams = self.ngrams(item["text"]) score = len(q_grams & doc_grams) / max(len(q_grams), 1) scored.append((score, idx)) scored.sort(reverse=True) return [self.items[i] for _, i in scored[:top_k] if _ > 0]

这里用二元字组的重叠度做相似度匹配,是纯朴素的实现,适合没有embeddings服务的场景。但要注意,真正的语义检索需要向量化,比如用某embedding模型把文本转成向量,再用余弦相似度找最近邻。只要对外暴露add和search两个方法,后续切换内部实现非常容易,这也是我把存储抽象成两个方法的原因。

实际使用时机也很关键,不是每轮对话都往长期记忆里写。我通常只在任务结束或者用户给出明确反馈时写入,避免把中间状态的噪音也存了进去。

3.3 任务拆解:从一次问答到多步执行

有些任务太复杂,比如“整理项目全部文档,输出一份周报”,靠模型在单个循环里可能绕来绕去。更稳妥的方式是把任务先拆成几个子任务,再逐个执行。

一种做法是外部编排:用一个额外模型调用生成子任务列表,然后Agent顺序执行。这个过程完全可以写代码实现。先准备一个拆解提示词:

PLAN_PROMPT = """把下面的用户目标拆解为最多3个可交给智能体执行的子任务。 每个子任务输出为一行,以 - 开头。 只输出任务列表,不要做额外解释。 用户目标:{goal} """

然后解析模型返回的文本,提取每行任务:

def parse_plan(raw: str): steps = [] for line in raw.strip().splitlines(): line = line.strip() if line.startswith(("-", "*")): steps.append(line.lstrip("-* ").strip()) elif any(line.startswith(f"{i}.") for i in range(1, 10)): steps.append(line.split(".", 1)[1].strip()) return steps

拿到子任务列表后,让一个Agent逐个执行,再把结果汇总给一个“总结Agent”整理输出。这就是很常见的主从式Agent结构。

举个例子,用户目标是“整理故障应急文档并生成建议清单”。拆解结果可能是:第一步,检索故障应急相关文档;第二步,读取文档摘要;第三步,提炼核心建议。每个子任务都是普通Agent最擅长的单步调用,整体执行质量和可控性都比让一个Agent自由发挥要好很多。

值得强调的是,拆解不是越细越好。子任务之间如果有依赖,拆太细反而增加通信成本;如果子任务之间没有共享上下文,还可能丢失关联信息。我一般控制在3到5个子任务,并且尽量让每个子任务目标单一、彼此独立。

4. 从Demo到生产:那些文档里不会写的实战坑

代码跑通只是第一步,真正麻烦的是让它稳定运行在生产环境。我在这里把踩过的一些坑集中说出来,每一个都是花钱买来的经验。

4.1 工具执行异常不能中断流程,要把错误回喂给模型

这是新手最容易犯的错。工具执行时如果遇到参数异常、文件不存在、网络超时,直接把异常抛出,Agent就会直接中断返回错误。但更好的做法是把异常信息转化成一条tool消息,让模型看到“工具执行失败了,原因是……”然后自己决定怎么办。

我在前面Agent代码里已经写了:

try: tool_output = self.registry.run(name, **args) except Exception as e: tool_output = {"error": str(e)}

这样模型看到错误信息后,通常会有两类反应:一是重新调整参数再试一次;二是换一个更合适的工具;三是诚实告诉用户当前能力不足。这三种结果都比直接抛异常强得多。我在实际调试中见过很多次,模型第一次调用工具参数写错,看到错误后第二次就自己纠正了。这种自愈能力,正是Agent区别于普通脚本的关键。

但也要设防,错误信息返回给模型,会导致模型反复用同样的错误参数重试。所以max_steps要设上限,并且可以在错误信息里附带一句“请不再尝试相同参数”,引导它换思路。

4.2 模型超时与限流:重试策略要讲究

大模型服务经常因为负载过高返回5xx错误,或者因为触发限流返回429。如果不做重试,线上任务会大量失败。但重试也不能无脑重试,必须做指数退避。

不推荐对401、403这类鉴权错误做重试,那是配置问题,重试一百次也没用。只对429、500、502、503、504这类瞬时错误重试比较合理。

def chat_with_retry(client: LLMClient, messages, tools, retries=3, base_delay=1.0): for attempt in range(retries): try: return client.chat(messages, tools) except requests.HTTPError as e: status = e.response.status_code if status in (429, 500, 502, 503, 504): time.sleep(base_delay * 2 ** attempt) continue raise except requests.Timeout: time.sleep(base_delay * 2 ** attempt) raise RuntimeError("模型调用多次重试失败")

有一个细节我特别想提醒:不要用固定间隔重试。当服务已经过载时,所有客户端同时用相同间隔重试,等于再制造一波流量冲击。指数退避能让客户端自动拉开请求时间,这是减轻服务压力的基本修养。

超时时间也要仔细调。Agent的每次调用都可能因为工具结果很长而变慢,我把超时设在60秒左右。如果你上游模型推理慢,还要适当拉长,否则会出现“服务其实在正常处理但客户端已经放弃”的假失败。

4.3 可观测性:给Agent装上日志探针

Debug Agent比Debug普通程序更难受,因为你不知道模型内部在“想什么”。唯一的办法是把每一步的外部行为记录清楚。我习惯给每个任务生成一个trace_id,贯穿整个链路。

import logging import uuid logger = logging.getLogger("agent") trace_id = uuid.uuid4().hex[:8] for step in range(max_steps): logger.info("[%s] step=%d messages_len=%d", trace_id, step, len(json.dumps(messages, ensure_ascii=False))) ... start = time.perf_counter() tool_output = registry.run(name, **args) latency_ms = (time.perf_counter() - start) * 1000 logger.info("[%s] tool=%s args=%s result=%s cost_ms=%.1f", trace_id, name, args, str(tool_output)[:200], latency_ms)

在日志里至少要看几类信息:当前step、消息体长度、调用的工具名、参数、返回结果、耗时。这些能让你快速判断问题属于哪一类:是模型反复调用工具(规划问题),是参数传错(工具定义问题),还是工具本身太慢(性能问题)。

还有一个建议是给工具结果做截断日志。有些工具返回很长文本,全量打日志会把日志文件撑爆。我通常只截取前200个字符记录,需要细节时再单独把完整结果写入调试文件。

4.4 权限与安全边界:工具不该什么都让你干

Agent的工具本质上是把一段代码的入口开放给模型,模型又受用户输入影响,这就意味着攻击者可能通过恶意构造用户输入,诱导模型调用危险工具。这个话题很容易被忽略,但一旦出事影响很严重。

我给自己定了几个基本原则。第一,工具运行环境要和核心系统隔离,至少不能用拥有生产库最高权限的账号去跑工具,我通常会在独立服务进程里执行工具代码。第二,对于写操作类工具,比如删除文件、修改配置、发送消息,必须在工具内部加二次确认机制,或者限制为“预览模式”。第三,从外部读取的文本如果可能包含敏感信息,在回填给模型之前先做脱敏处理。第四,永远不要在系统提示词或日志里出现密钥、证书、内部地址等敏感信息。

不要觉得这些步骤繁琐,Agent越强大,它被滥用的破坏力就越大。工具注册表加一个字段来表示“权限等级”,执行前统一校验,是很简单但很有价值的做法。

5. 成本控制、效果评估与后续扩展方向

5.1 token成本怎么估算与控制

Agent每走一步,messages里追加的内容都会被完整发到模型端,所以token消耗比普通对话要快得多。一个5步工具调用的任务,光是消息体本身体积就可能翻几倍,再加上tools描述,成本一下就上来了。

业界有个粗略估算方法:消息体JSON字符串的长度除以3,可以当做一个近似的token数,因为1个英文字符约等于0.25到0.3个token,但这只是粗略值,中文字符占用更多。真实成本看服务商返回的usage字段最准。

代码里可以写一个小函数做前置预算:

def estimate_tokens(messages): text = json.dumps(messages, ensure_ascii=False) return len(text) // 3

控制token成本有几个方向。第一,精简工具描述,每个工具的description控制在50字以内,schema只保留必要字段。第二,缩短工具结果回填长度,比如文件检索结果只保留前几个字段,不要整个文件内容都塞回去。第三,及时使用短期记忆模块裁剪早期消息。第四,为不同任务选择不同规格的模型,简单检索任务用轻量模型,复杂规划任务用强模型。

我在一个内部项目里做过一个对比:同样的任务,不裁剪消息时平均消耗约1万token,加了滑动窗口压缩后降到4000多。这个差距在每日上万次调用时就是实打实的成本差距。

5.2 如何给Agent建立效果评估体系

Agent效果评估比普通算法模型更主观,但不能因此就不评估。我通常准备一个固定任务集,大约十来个典型场景,每次改动架构或提示词后都跑一遍。评价指标如下:

指标含义我关注的原因
任务完成率最终是否产出有效结果最核心指标,反映整体可用性
平均调用步数完成任务用了多少轮工具调用步数越多成本越高、失败风险越大
工具错误率工具调用时报错的比例报错多往往代表工具定义或上下文有问题
上下文峰值长度任务过程中messages最长的时刻防止模型卡在长上下文里溢出
单次任务成本估算token数或费用决定方案能否规模化

有一点很重要:测试用例要覆盖边界场景。比如“工具没有返回结果时模型会不会诚实告知”“工具连续失败时模型会不会换策略”,这些比测试理想场景更能暴露问题。

我自己调试时常做的一件事,就是把失败案例的完整日志保存下来,过一段时间集中复盘。有时候你会惊讶地发现,同一个问题反复出现,原因往往不是模型笨,而是某个工具描述一直有歧义。

5.3 什么时候该把单体Agent拆成多Agent

文章开头提到多Agent,很多人会觉得那是架构的终极形态,但其实大部分业务阶段不需要它。我判断的标准是三条:工具数量超过二十个且互相之间职责差异大、不同任务对模型能力要求差距明显、任务本身有明显的阶段划分且需要隔离权限。

满足任何一条,再考虑拆分。典型拆分方式是先把工具按领域分群,比如“知识检索类Agent”管搜索、摘要、文档读取,“业务执行类Agent”管下单、审批、数据变更。上层有一个规划器负责把用户请求分发给对应Agent。

拆多Agent的好处是职责单一、权限好控制、上下文干净,代价是通信成本增加、排查问题链路变长。所以我的建议始终是:先单体跑起来,观察真实瓶颈,再决定是否拆分。没有一个Agent框架能替你做这个业务判断。

把上面这些代码和思路组合起来,你会发现Agent工程其实不复杂:一个注册表、一个循环、一套记忆管理,再加上工程层面的容错和可观测性,就已经是一个相当完整的智能体底座了。后续加规划器、加长期向量库、加多Agent协作,都是在这个底座上做扩展,而不是推翻重来。我自己在维护这类系统时,最大的感受就是“稳定压倒一切”:先让它在小范围内可靠地完成任务,再慢慢扩大它的工具边界和能力范围。

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

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

立即咨询