news 2026/10/10 4:01:05

从0到1构建可运行Agent系统:架构、工具调用与工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从0到1构建可运行Agent系统:架构、工具调用与工程实践

1. 为什么"会聊天"和"会干活"是两码事

很多人第一次接触Agent这个概念时,脑子里浮现的画面是科幻电影里那种能陪你聊天、帮你查天气的语音助手。但真正上手做项目之后你会发现,Agent的核心价值根本不在于"聊得好",而在于"干得成"。这两者之间的差距,比大多数人想象的要大得多。

我刚开始接触Agent开发的时候,踩过的第一个坑就是把它当成了一个"更聪明的聊天机器人"。当时我的想法很简单:既然大模型已经能理解自然语言了,那我只要给它一个目标,它不就能自己规划步骤、调用工具、完成任务了吗?结果实测下来,模型确实能给出一个看起来像模像样的计划,但一旦进入执行环节,各种问题就冒出来了——工具调用参数格式不对、多步任务中间状态丢失、遇到异常不知道怎么回退、循环调用同一个工具直到超时。

这些问题的根源在于:语言理解和任务执行是两个完全不同的能力维度。大模型擅长的是前者,而Agent系统需要的是后者。一个能跑通的Agent,本质上是一个"以大模型为决策核心的工程系统",它需要记忆管理、工具编排、状态追踪、错误恢复、终止条件判断等一系列工程组件的配合。模型只是这个系统里的一个零件,虽然是最重要的那个,但绝不是全部。

所以这篇文章想做的事情很明确:不讲空泛的概念,不堆砌术语,而是从一个实际可运行的最小Agent系统出发,把从0到1跑通一个Agent任务所需要的每一个环节都拆开来讲。不管你是刚接触Agent开发的新手,还是已经尝试过一些框架但总觉得"跑不通"的开发者,都能从里面找到可以直接用的东西。

文章会覆盖Agent的核心架构设计、工具调用的实现细节、记忆与状态的工程处理、任务规划的策略选择、以及实测中最容易踩的那些坑。每一部分都会给出具体的代码示例和参数说明,力求让你看完之后能直接动手复现。

2. Agent系统的四层架构拆解

2.1 从"输入到输出"看Agent的完整链路

要理解Agent怎么跑通,最直观的方式是跟踪一次完整的任务执行链路。假设我给Agent下达一个任务:"帮我查一下北京今天天气,如果温度低于10度就提醒我带外套。"

这个任务看起来简单,但拆开来看,Agent需要完成以下步骤:

第一步,意图解析。模型需要理解这个任务包含了一个条件判断(温度低于10度)和一个条件动作(提醒带外套)。这不是简单的"查天气"指令,而是一个带有逻辑分支的复合任务。

第二步,工具选择。Agent需要知道有哪些工具可用。假设我们给它注册了一个天气查询工具,它需要判断这个任务应该调用哪个工具、传什么参数。

第三步,工具调用。Agent生成工具调用的请求,包括工具名称和参数。这里的关键是参数格式必须和工具定义完全匹配,否则调用会失败。

第四步,结果解析。工具返回结果后,Agent需要理解返回的数据,判断条件是否满足。

第五步,条件执行。如果温度确实低于10度,Agent需要生成提醒;如果高于10度,则不需要。

第六步,终止判断。任务完成后,Agent需要知道什么时候停下来,而不是无限循环。

这六个步骤构成了Agent执行的最小闭环。任何一个环节出问题,整个任务就会失败。而实际项目中,最容易出问题的往往是第三步和第六步——工具调用的参数格式和终止条件的判断。

2.2 决策层、工具层、记忆层、编排层的职责边界

把上面的链路抽象一下,一个Agent系统可以分成四个层次:

决策层由大模型担任,负责理解任务、规划步骤、选择工具、判断条件。这一层的核心挑战是"如何让模型的输出格式稳定可控"。因为模型输出是自然语言,而工程系统需要的是结构化数据,这中间的转换需要精心设计的提示词和输出解析逻辑。

工具层是Agent与外部世界交互的接口。每个工具本质上是一个函数,有明确的输入参数和输出格式。工具层的关键设计原则是"单一职责"——一个工具只做一件事,参数尽量简单,返回值尽量结构化。我见过很多新手把工具设计得过于复杂,一个工具接受七八个参数,结果模型经常传错参数,调试起来非常痛苦。

记忆层负责管理Agent的短期和长期记忆。短期记忆就是当前任务的对话历史和中间状态,长期记忆则是跨任务的知识积累。短期记忆的管理核心是"上下文窗口的合理利用"——不能把所有历史都塞进去,也不能丢太多导致模型丢失关键信息。

编排层是Agent的"调度中心",负责控制整个执行流程:什么时候调用模型、什么时候执行工具、什么时候检查终止条件、出错时怎么处理。编排层的设计直接决定了Agent的稳定性和可控性。

这四个层次的职责边界必须清晰。我踩过的一个坑就是把太多逻辑塞进了提示词里,试图让模型自己管理状态和流程,结果就是模型经常"忘记"之前的步骤,或者陷入死循环。后来我把状态管理和流程控制从模型手里拿走,交给编排层用代码实现,稳定性立刻上了一个台阶。

2.3 最小可运行Agent的代码骨架

下面是一个最小可运行Agent的代码骨架,用Python实现,不依赖任何Agent框架,纯手写编排逻辑。这样做的目的是让你看清楚每一层在做什么,而不是被框架的抽象层遮住视线。

import json from typing import Callable class SimpleAgent: def __init__(self, llm_client, tools: dict[str, Callable], max_steps: int = 10): self.llm = llm_client self.tools = tools self.max_steps = max_steps self.history = [] def run(self, task: str) -> str: self.history.append({"role": "user", "content": task}) for step in range(self.max_steps): # 1. 调用模型,获取下一步动作 response = self.llm.chat( messages=self._build_prompt(), tools=self._tool_schemas() ) # 2. 如果模型返回的是最终答案,结束 if response.type == "final_answer": return response.content # 3. 如果模型要求调用工具,执行工具 if response.type == "tool_call": tool_name = response.tool_name tool_args = response.tool_args if tool_name not in self.tools: self.history.append({ "role": "tool", "content": f"错误:工具 {tool_name} 不存在" }) continue try: result = self.tools[tool_name](**tool_args) self.history.append({ "role": "tool", "content": json.dumps(result, ensure_ascii=False) }) except Exception as e: self.history.append({ "role": "tool", "content": f"工具执行错误:{str(e)}" }) return "任务未在最大步数内完成" def _build_prompt(self): # 构建包含系统提示、工具说明、历史记录的完整提示 pass def _tool_schemas(self): # 返回工具的结构化描述,供模型选择 pass

这段代码虽然简单,但包含了Agent系统的核心要素:循环控制、工具调用、错误处理、终止条件。你可以在这个骨架上逐步添加记忆管理、多轮规划、结果验证等功能。

关键设计点在于max_steps这个参数。它防止Agent陷入无限循环,是系统稳定性的最后一道防线。我建议这个值不要设得太大,一般5到10步就够了。如果一个任务需要超过10步才能完成,那要么是任务本身太复杂需要拆分,要么是Agent的规划能力有问题需要优化提示词。

3. 工具调用:Agent真正"动手"的关键环节

3.1 工具描述怎么写模型才不犯迷糊

工具调用是Agent从"会说"到"会做"的分水岭。而工具调用能不能成功,很大程度上取决于工具描述写得好不好。

我见过很多新手写工具描述的方式是这样的:

def query_weather(city): """查询天气""" ...

然后抱怨模型总是传错参数或者选错工具。问题出在描述太模糊了。"查询天气"这四个字没有告诉模型:这个工具接受什么参数?参数格式是什么?返回什么数据?什么场景下应该用这个工具而不是别的?

一个好的工具描述应该包含以下要素:

def query_weather(city: str, date: str = "today") -> dict: """ 查询指定城市指定日期的天气信息。 参数: city: 城市名称,必须是中文城市名,如"北京"、"上海"。 date: 日期,格式为"YYYY-MM-DD",默认为"today"表示今天。 返回: 包含以下字段的字典: - temperature: 温度(摄氏度,整数) - condition: 天气状况(如"晴"、"多云"、"小雨") - humidity: 湿度(百分比,整数) 使用场景: 当用户询问天气、温度、是否需要带伞、是否适合出行等问题时使用。 不要用于查询历史天气数据或未来超过7天的天气预报。 """ ...

这个描述告诉模型四件事:参数是什么、参数格式是什么、返回什么、什么时候用。这四件事缺一不可。

特别要强调的是"使用场景"这一段。很多人会忽略它,但它其实是减少工具误用的关键。模型在选择工具时,最困惑的往往是"这个工具和那个工具都能做类似的事情,我该选哪个"。明确的场景说明能帮它做出正确选择。

还有一个实用技巧:在工具描述里加入反例。比如"不要用于查询历史天气数据",这能有效防止模型在不该用这个工具的时候调用它。

3.2 参数校验与容错:让Agent不那么容易崩

即使工具描述写得再好,模型偶尔还是会传错参数。这时候如果工具直接抛异常,整个Agent流程就会中断。所以参数校验和容错机制是必须的。

我的做法是在工具函数内部做三层校验:

第一层是类型校验。检查传入的参数类型是否正确。比如期望是字符串但传了数字,期望是列表但传了字符串。

第二层是格式校验。检查参数的格式是否符合要求。比如日期格式是否正确、城市名是否在支持列表中。

第三层是业务校验。检查参数在业务逻辑上是否合理。比如查询的日期是否在未来7天内、温度值是否在合理范围内。

每一层校验失败时,不要直接抛异常,而是返回一个结构化的错误信息,让模型知道哪里错了、应该怎么改。这样模型就有机会在下一轮修正自己的调用。

def query_weather(city: str, date: str = "today") -> dict: # 类型校验 if not isinstance(city, str): return {"error": "city参数必须是字符串", "suggestion": "请传入中文城市名,如'北京'"} # 格式校验 supported_cities = ["北京", "上海", "广州", "深圳", "杭州"] if city not in supported_cities: return { "error": f"暂不支持查询{city}的天气", "suggestion": f"目前支持的城市有:{', '.join(supported_cities)}" } # 业务校验 if date != "today": try: from datetime import datetime, timedelta query_date = datetime.strptime(date, "%Y-%m-%d") if query_date > datetime.now() + timedelta(days=7): return {"error": "只能查询未来7天内的天气", "suggestion": "请调整查询日期"} except ValueError: return {"error": "日期格式错误", "suggestion": "请使用YYYY-MM-DD格式"} # 实际查询逻辑 ...

这种"返回错误而不是抛异常"的设计,让Agent有了自我修正的机会。实测下来,大部分参数错误都能在1到2轮内被模型自行修正。

3.3 多工具编排时的优先级与冲突处理

当Agent注册了多个工具时,工具之间的优先级和冲突处理就变成了一个必须考虑的问题。

最常见的冲突场景是:两个工具的功能有重叠。比如你有一个"查询天气"工具和一个"查询综合信息"工具,后者也能返回天气数据。模型在面对"今天天气怎么样"这个问题时,可能会随机选择其中一个。

解决这个问题的办法有三个:

第一,在工具描述中明确优先级。比如在"查询综合信息"的描述中写明"如果只需要天气信息,请优先使用query_weather工具"。

第二,在系统提示词中给出选择规则。比如"当有专用工具可用时,优先使用专用工具而不是通用工具"。

第三,在编排层做后处理。如果检测到模型选择了非最优工具,可以拦截并重新引导。不过这种做法会增加系统复杂度,一般不建议在初期使用。

除了冲突,还要考虑工具的依赖关系。有些工具的输出是另一些工具的输入。比如"查询航班"工具返回的航班号,可能需要传给"查询航班状态"工具。这种依赖关系如果让模型自己管理,很容易出错。更好的做法是在编排层维护一个"工具调用图",明确哪些工具可以独立调用、哪些需要前置条件。

我在一个项目中遇到过这样的情况:Agent需要先查询用户ID,再用用户ID查询订单信息。模型有时候会跳过第一步直接查订单,导致参数缺失。后来我在编排层加了一个简单的依赖检查:如果调用的工具需要某个参数但上下文中没有,就自动触发前置工具的调用。这个改动把任务成功率从60%提升到了90%以上。

4. 记忆与状态:让Agent不再"失忆"

4.1 短期记忆的窗口管理策略

Agent的短期记忆就是当前任务的对话历史。它记录了用户说了什么、Agent做了什么、工具返回了什么。这些信息是模型做下一步决策的依据。

但上下文窗口是有限的。当对话轮次增多时,历史记录会越来越长,最终超出模型的上下文限制。这时候就需要做窗口管理。

常见的策略有三种:

滑动窗口是最简单的做法:只保留最近N轮对话,更早的直接丢弃。这种策略的优点是实现简单,缺点是可能丢失关键信息。比如任务开始时用户提到的某个约束条件,可能在几轮之后就被滑出了窗口。

摘要压缩是更聪明的做法:当历史记录超过一定长度时,用模型把早期对话压缩成一段摘要,保留关键信息,丢弃细节。这样可以在有限的窗口内容纳更多的有效信息。缺点是摘要过程本身需要调用模型,增加了延迟和成本。

关键信息提取是我最推荐的做法:在每轮对话结束后,用规则或轻量模型提取出关键信息(如用户偏好、任务约束、已完成的步骤),存储在一个独立的结构化字段中。这些关键信息始终保留在上下文中,而原始对话历史则可以用滑动窗口管理。

class MemoryManager: def __init__(self, max_history: int = 10): self.max_history = max_history self.history = [] self.key_facts = {} # 结构化关键信息 def add_message(self, role: str, content: str): self.history.append({"role": role, "content": content}) self._extract_key_facts(role, content) # 滑动窗口 if len(self.history) > self.max_history: self.history = self.history[-self.max_history:] def _extract_key_facts(self, role: str, content: str): # 从对话中提取关键信息 # 可以用规则匹配,也可以用轻量模型 pass def get_context(self): # 返回给模型的完整上下文 context = [] if self.key_facts: context.append({ "role": "system", "content": f"已知关键信息:{json.dumps(self.key_facts, ensure_ascii=False)}" }) context.extend(self.history) return context

这种"滑动窗口+关键信息提取"的组合策略,在实际项目中效果最好。它既控制了上下文长度,又保证了关键信息不丢失。

4.2 跨任务状态传递的工程实现

单个任务内的记忆管理相对简单,跨任务的状态传递才是真正考验工程能力的地方。

所谓跨任务状态传递,是指Agent在执行任务A时获得的信息,在执行任务B时还能用上。比如用户第一次让Agent查了北京的天气,第二次让Agent"帮我推荐一下今天适合穿什么",Agent需要记住第一次查询的结果。

实现跨任务状态传递的核心是持久化存储。每次任务结束后,把关键状态写入数据库或文件;下次任务开始时,根据任务类型加载相关的状态。

状态存储的设计要注意几点:

第一,状态要有明确的过期策略。天气信息可能几小时后就失效了,用户偏好可能几个月都不变。不同类型的状态需要不同的过期时间。

第二,状态要有作用域。有些状态是全局的(如用户偏好),有些是会话级的(如当前对话的上下文),有些是任务级的(如某个任务的中间结果)。作用域混乱会导致状态污染。

第三,状态要有版本控制。当状态结构发生变化时,旧版本的状态需要能够平滑迁移或安全丢弃。

class StateStore: def __init__(self, storage_backend): self.storage = storage_backend def save(self, key: str, value: dict, scope: str, ttl: int = None): """ scope: 'global' | 'session' | 'task' ttl: 过期时间(秒),None表示永不过期 """ record = { "value": value, "scope": scope, "created_at": time.time(), "ttl": ttl } self.storage.set(key, json.dumps(record)) def load(self, key: str) -> dict | None: raw = self.storage.get(key) if not raw: return None record = json.loads(raw) if record["ttl"] and time.time() - record["created_at"] > record["ttl"]: self.storage.delete(key) return None return record["value"]

这个简单的状态存储实现,配合合理的key命名规范(如user:{user_id}:preference、session:{session_id}:context),就能满足大部分跨任务状态传递的需求。

4.3 上下文膨胀的预防与压缩

上下文膨胀是Agent开发中最隐蔽的问题之一。它不会导致系统崩溃,但会让Agent变得越来越慢、越来越贵、越来越不准确。

上下文膨胀的典型表现是:任务执行到第5步时,上下文里已经塞满了前4步的工具返回结果、错误信息、重试记录。模型需要在大量无关信息中找到关键信息,注意力被严重分散。

预防上下文膨胀的核心原则是:只保留对下一步决策有用的信息。

具体做法包括:

  • 工具返回结果只保留关键字段,丢弃冗余数据。比如天气查询返回了20个字段,但Agent只需要温度和天气状况,那就只保留这两个。
  • 错误信息在模型修正后就可以丢弃,不需要一直保留在上下文中。
  • 中间步骤的详细过程可以压缩成一句话摘要,如"已完成天气查询,北京今天5度,晴"。

我通常会在编排层加一个compress_context函数,在每轮对话结束后自动执行压缩:

def compress_context(history: list) -> list: """压缩对话历史,保留关键信息""" compressed = [] for msg in history: if msg["role"] == "tool": # 工具返回结果只保留前200字符 content = msg["content"][:200] if len(msg["content"]) > 200: content += "...(已截断)" compressed.append({"role": "tool", "content": content}) elif msg["role"] == "assistant" and "tool_call" in msg: # 工具调用记录压缩成简短描述 compressed.append({ "role": "assistant", "content": f"[调用了{msg['tool_name']}工具]" }) else: compressed.append(msg) return compressed

这个压缩策略在实测中能把上下文长度减少40%到60%,同时几乎不损失关键信息。

5. 任务规划:从"走一步看一步"到"心中有数"

5.1 ReAct模式的实际落地细节

ReAct(Reasoning + Acting)是目前最主流的Agent任务规划模式。它的核心思想是让模型在每一步都先"思考"再"行动":思考当前状态、决定下一步做什么、执行动作、观察结果、继续思考。

这个模式听起来简单,但实际落地时有很多细节需要注意。

思考过程的格式要固定。如果让模型自由发挥,它每次的思考格式都不一样,后续解析会很困难。我通常会在提示词中明确规定思考的格式,比如:

请按以下格式输出: 思考:[你的推理过程] 行动:[工具名称] 参数:[JSON格式的参数]

思考和行动要分离。有些实现会让模型在思考中直接嵌入行动,比如"我需要查询天气,所以我调用query_weather工具"。这种混合格式解析起来很麻烦。更好的做法是让思考和行动成为两个独立的字段。

观察结果要简洁。工具返回的结果往往包含大量数据,但模型只需要其中的关键信息。在把结果喂回给模型之前,先做一轮过滤和摘要。

循环要有明确的退出条件。ReAct模式天然是一个循环,如果没有退出条件,模型可能会一直"思考-行动"下去。退出条件通常有两个:模型输出了最终答案,或者达到了最大步数。

def react_loop(agent, task, max_steps=10): history = [{"role": "user", "content": task}] for step in range(max_steps): # 构建ReAct提示 prompt = build_react_prompt(history) response = agent.llm.chat(prompt) # 解析模型输出 parsed = parse_react_output(response) if parsed["type"] == "final_answer": return parsed["answer"] if parsed["type"] == "action": # 执行工具 try: result = agent.tools[parsed["tool"]](**parsed["args"]) observation = summarize_result(result) except Exception as e: observation = f"执行失败:{str(e)}" # 把思考和观察加入历史 history.append({"role": "assistant", "content": response}) history.append({"role": "user", "content": f"观察结果:{observation}"}) return "达到最大步数,任务未完成"

这个循环的关键在于parse_react_output函数。它需要健壮地处理模型输出的各种变体。我建议用正则表达式做初步解析,失败时再用模型做二次解析作为兜底。

5.2 Plan-and-Execute的适用场景与取舍

ReAct模式是"走一步看一步",适合步骤不确定、需要根据中间结果动态调整的任务。但有些任务步骤是明确的,这时候Plan-and-Execute模式更合适。

Plan-and-Execute的核心思想是:先让模型制定一个完整的计划,然后按计划逐步执行。执行过程中如果遇到问题,再回到规划阶段重新制定计划。

这种模式的优势在于:

  • 全局视野:模型在制定计划时能看到任务全貌,避免"只见树木不见森林"。
  • 效率更高:不需要每一步都调用模型做决策,可以减少模型调用次数。
  • 可解释性更强:计划是显式的,用户可以清楚地看到Agent打算怎么做。

但它的劣势也很明显:

  • 灵活性差:如果执行过程中出现计划外的情况,需要重新规划,成本较高。
  • 对规划能力要求高:如果初始计划质量不高,后续执行会步步艰难。

我的经验是:步骤明确、依赖关系清晰的任务用Plan-and-Execute;步骤不确定、需要探索的任务用ReAct。比如"帮我订一张明天从北京到上海的机票"适合Plan-and-Execute,因为步骤基本固定(查航班、选航班、填信息、支付);而"帮我分析一下这份销售数据有什么异常"更适合ReAct,因为分析路径取决于数据本身的特点。

实际项目中,我经常把两种模式混合使用:先用Plan-and-Execute制定高层计划,每个高层步骤内部用ReAct灵活执行。这样既有全局视野,又有局部灵活性。

5.3 规划失败的降级策略

再好的规划也可能失败。工具不可用、参数错误、外部服务超时、模型判断失误——这些都会导致计划执行不下去。这时候如果没有降级策略,整个Agent就会卡死。

我通常会在三个层面设置降级策略:

第一层:重试。对于临时性错误(如网络超时),直接重试。重试次数一般设为2到3次,每次重试之间加一个短暂的等待。

第二层:替代方案。对于工具不可用的情况,尝试用其他工具替代。比如天气查询工具挂了,可以尝试用综合信息查询工具。这需要在工具注册时就定义好替代关系。

第三层:任务降级。如果核心步骤无法完成,尝试完成一个简化版的任务。比如"帮我订机票并选座"如果选座失败,至少把机票订了,然后告诉用户选座需要手动操作。

class FallbackStrategy: def __init__(self, agent): self.agent = agent self.retry_count = {} def execute_with_fallback(self, tool_name, args, max_retries=2): # 第一层:重试 for attempt in range(max_retries): try: return self.agent.tools[tool_name](**args) except TemporaryError: if attempt < max_retries - 1: time.sleep(1) continue break except PermanentError as e: break # 第二层:替代工具 alternatives = self.agent.get_alternatives(tool_name) for alt_tool in alternatives: try: return self.agent.tools[alt_tool](**args) except Exception: continue # 第三层:返回降级结果 return { "status": "degraded", "message": f"工具{tool_name}暂时不可用,已尝试替代方案但均失败", "suggestion": "请稍后重试或手动完成此步骤" }

这套降级策略看起来简单,但在实际项目中能把任务成功率提升20%以上。关键是要在系统设计初期就考虑到失败的可能性,而不是等到出问题了再临时加处理逻辑。

6. 实测中最容易踩的五个坑

6.1 工具返回结果太长导致模型"看花眼"

这是我在多个项目中反复遇到的问题。工具返回的JSON数据动辄几百上千字,模型在处理这些数据时经常抓不住重点。

有一次我做一个数据分析Agent,查询工具返回了包含50个字段的详细数据。模型在后续推理中,居然把"数据更新时间"当成了"数据值"来使用,导致整个分析结果完全错误。

解决这个问题的办法是在工具层做结果裁剪。工具返回的数据应该只包含Agent决策所需的最小字段集。如果确实需要返回大量数据,就在工具内部先做一轮摘要,只把摘要返回给模型。

def query_sales_data(region: str, period: str) -> dict: raw_data = fetch_from_database(region, period) # 返回50个字段 # 只返回Agent需要的字段 return { "region": raw_data["region"], "period": raw_data["period"], "total_sales": raw_data["total_sales"], "growth_rate": raw_data["growth_rate"], "top_product": raw_data["top_product"], "anomaly_detected": raw_data["anomaly_flag"] }

如果模型确实需要查看详细数据,可以提供一个单独的"查看详情"工具,让模型在需要时主动调用。这样既保证了默认情况下的简洁性,又保留了深入查看的能力。

6.2 模型"自作主张"调用未注册的工具

这个问题在使用了多个Agent框架的项目中特别常见。模型在训练数据中见过很多工具名称,有时候会"幻觉"出一个不存在的工具来调用。

比如你只注册了query_weather工具,但模型输出了get_weather_forecast。如果你没有做校验,直接去工具字典里查找,就会抛出KeyError。

解决办法很简单:在工具调用前做白名单校验。如果模型调用的工具不在注册列表中,不要直接报错,而是返回一个友好的提示,告诉模型有哪些工具可用。

def handle_tool_call(self, tool_name, args): if tool_name not in self.tools: available = ", ".join(self.tools.keys()) return { "error": f"工具'{tool_name}'不存在", "available_tools": available, "suggestion": "请从可用工具列表中选择" } return self.tools[tool_name](**args)

这个简单的校验能避免大部分因为工具名幻觉导致的崩溃。而且返回可用工具列表后,模型通常能在下一轮选择正确的工具。

6.3 多轮对话中指令被"淹没"

当对话轮次增多时,最初的指令可能会被后续的大量信息淹没。模型在生成回复时,注意力被最近的内容吸引,忘记了最初的任务目标。

我遇到过一个典型案例:用户要求Agent"用中文回复,不要用英文"。前几轮Agent确实用中文回复,但到了第5轮之后,Agent突然开始夹杂英文。原因就是最初的语言指令已经被淹没在大量的工具返回结果中了。

解决办法是把关键指令放在系统提示词中,而不是用户消息中。系统提示词在每一轮对话中都会被重复,不会被历史记录淹没。

system_prompt = """你是一个任务执行Agent。请遵守以下规则: 1. 始终用中文回复用户。 2. 每次只调用一个工具。 3. 工具调用失败时,先分析原因再重试。 4. 任务完成后,用简洁的语言总结结果。 """

如果某些指令确实需要放在用户消息中(比如任务特定的约束),可以在编排层定期把这些约束重新注入到上下文中。比如每3轮对话后,把最初的用户指令重新附加到当前消息前面。

6.4 终止条件判断失误导致的死循环

死循环是Agent开发中最危险的问题之一。它不仅浪费资源,还可能导致系统崩溃。

死循环的常见原因有三种:

第一种是模型反复调用同一个工具。比如查询天气失败后,模型不断重试同一个查询,而不去分析失败原因。

第二种是模型在两个工具之间反复横跳。比如先调用工具A,发现需要工具B的结果,调用工具B后又发现需要工具A的结果,如此循环。

第三种是模型无法判断任务是否完成。它一直在"思考"和"行动",但永远不输出最终答案。

解决死循环需要多层防护:

  • 最大步数限制:这是最基本的防护,超过步数直接终止。
  • 重复调用检测:如果连续3次调用同一个工具且参数相同,强制终止或要求模型换一种方式。
  • 循环检测:记录工具调用的序列,如果出现重复的模式(如A-B-A-B),强制终止。
  • 进度评估:每轮对话后评估任务是否在推进,如果连续几轮没有实质性进展,触发终止。
class LoopDetector: def __init__(self, window_size=6): self.call_history = [] self.window_size = window_size def check(self, tool_name, args) -> bool: """返回True表示检测到循环""" call_signature = f"{tool_name}:{json.dumps(args, sort_keys=True)}" self.call_history.append(call_signature) if len(self.call_history) < self.window_size: return False recent = self.call_history[-self.window_size:] # 检测完全重复的调用 if len(set(recent)) == 1: return True # 检测ABAB模式 if len(recent) >= 4: if recent[-4] == recent[-2] and recent[-3] == recent[-1]: return True return False

这个检测器在实际项目中帮我避免了很多次死循环。关键是要在检测到循环时给出明确的提示,让模型知道自己在循环,并引导它换一种策略。

6.5 工具调用超时拖垮整个流程

外部工具调用超时是另一个常见问题。特别是当工具依赖外部API时,网络延迟、服务限流、对方系统故障都可能导致调用超时。

如果没有超时处理,整个Agent流程就会卡在等待工具返回的状态,用户看到的就是"Agent无响应"。

处理超时有两个层面:

第一层是设置合理的超时时间。不同的工具需要不同的超时时间。查询本地数据库可能只需要1秒,调用外部API可能需要10秒。超时时间要根据工具的实际特性来设置,不能一刀切。

第二层是超时后的处理。超时后不能直接放弃,而是要告诉模型"这个工具超时了",让模型决定是重试、换工具还是跳过这一步。

import signal def call_with_timeout(func, args, timeout_seconds=10): def handler(signum, frame): raise TimeoutError(f"工具调用超时({timeout_seconds}秒)") signal.signal(signal.SIGALRM, handler) signal.alarm(timeout_seconds) try: result = func(**args) signal.alarm(0) # 取消定时器 return result except TimeoutError as e: return { "error": str(e), "suggestion": "可以尝试重试,或使用替代工具,或跳过此步骤" } finally: signal.alarm(0)

需要注意的是,signal.alarm只在Unix系统上有效。如果在Windows上运行,需要用threading.Timer来实现类似的功能。另外,超时处理要配合重试策略一起使用,单次超时不一定意味着工具不可用,可能是临时的网络波动。

7. 从跑通到跑稳:我的调优经验

7.1 提示词迭代的实用方法

Agent的提示词不是写一次就完事的,需要反复迭代。但迭代不能靠感觉,要有系统的方法。

我的做法是建立一个测试用例集。收集20到30个典型任务,覆盖不同的任务类型、不同的复杂度、不同的边界情况。每次修改提示词后,跑一遍测试集,看成功率的变化。

测试用例要包含以下几类:

  • 简单任务:单步就能完成的,如"查一下北京天气"。
  • 多步任务:需要多个工具配合的,如"查天气并推荐穿搭"。
  • 条件任务:包含条件判断的,如"如果温度低于10度就提醒我带外套"。
  • 异常任务:工具会失败或返回异常的,如"查询一个不存在的城市"。
  • 边界任务:参数处于边界值的,如"查询未来第7天的天气"。

每次修改提示词后,记录每类任务的成功率。如果某类任务的成功率下降,说明修改引入了问题。如果整体成功率提升但某类任务下降,需要权衡是否值得。

提示词迭代的一个关键原则是:每次只改一个地方。如果一次改了好几个地方,就无法判断是哪个改动导致了效果变化。这跟做实验是一个道理,要控制变量。

7.2 日志与可观测性建设

Agent系统的调试比传统程序困难得多,因为它的行为有很大的不确定性。同样的输入,模型可能给出不同的输出。没有完善的日志,出了问题根本无从查起。

我建议在Agent系统中记录以下日志:

  • 每轮对话的完整输入输出:包括系统提示词、用户消息、模型回复、工具调用请求和结果。
  • 每步的耗时:模型调用耗时、工具执行耗时、总耗时。
  • 每步的token消耗:输入token数、输出token数、累计token数。
  • 错误和异常:错误类型、错误信息、发生时的上下文。
  • 决策路径:模型选择了哪个工具、为什么选择、是否触发了降级策略。

这些日志不仅能用于调试,还能用于优化。比如通过分析token消耗,可以发现哪些步骤的上下文过长需要压缩;通过分析耗时,可以发现哪些工具是性能瓶颈。

import logging import time class AgentLogger: def __init__(self, log_file: str): self.logger = logging.getLogger("agent") handler = logging.FileHandler(log_file) handler.setFormatter(logging.Formatter( "%(asctime)s | %(levelname)s | %(message)s" )) self.logger.addHandler(handler) self.logger.setLevel(logging.INFO) def log_step(self, step: int, action: str, detail: dict, duration: float): self.logger.info(json.dumps({ "step": step, "action": action, "detail": detail, "duration_ms": round(duration * 1000, 2) }, ensure_ascii=False))

日志的格式建议用JSON,方便后续用脚本做分析和统计。如果日志量很大,可以考虑接入专门的日志分析工具。

7.3 性能与成本的平衡

Agent系统的运行成本主要来自两个方面:模型调用的token费用和工具调用的外部服务费用。在保证效果的前提下控制成本,是每个Agent开发者都要面对的问题。

降低成本的几个实用策略:

第一,用小模型做简单决策。不是所有步骤都需要用最强的大模型。工具选择、参数提取这类相对简单的任务,可以用小模型完成。只有复杂的推理和规划才需要用大模型。

第二,缓存重复的工具调用。如果多个任务查询了相同的天气数据,没必要每次都调用工具。可以在工具层加一个缓存,相同参数的调用在短时间内直接返回缓存结果。

第三,压缩上下文。前面提到的上下文压缩策略,不仅能提升效果,还能直接降低token消耗。上下文越短,每次模型调用的费用越低。

第四,设置合理的最大步数。最大步数设得太大,不仅浪费资源,还增加了死循环的风险。根据任务的实际复杂度设置合理的上限。

第五,批量处理。如果有多个独立的任务需要执行,可以考虑批量处理,减少模型调用的次数。

我在一个项目中通过组合使用这些策略,把单次任务的平均成本从0.5元降到了0.15元,同时任务成功率还略有提升。关键是要在系统设计初期就把成本作为一个考量因素,而不是等到账单出来了才想办法。

7.4 什么任务适合交给Agent,什么不适合

最后想聊一个容易被忽略的问题:不是所有任务都适合用Agent来做。

Agent适合的任务类型有以下特征:

  • 步骤不确定:完成任务需要多少步、走什么路径,事先无法完全确定。
  • 需要外部信息:任务执行过程中需要查询外部数据或调用外部服务。
  • 有一定的容错空间:任务失败后可以重试,或者部分完成也有价值。
  • 自然语言交互:用户用自然语言描述任务,而不是结构化指令。

不适合Agent的任务类型包括:

  • 步骤完全固定:如果任务的每一步都是确定的,用传统的工作流引擎更可靠、更高效。
  • 对准确性要求极高:Agent的行为有不确定性,如果任务不允许任何错误(如金融交易),需要加入大量校验和人工确认环节。
  • 实时性要求极高:Agent的每一步都需要调用模型,延迟通常在秒级。如果任务要求毫秒级响应,Agent不适合。
  • 成本极度敏感:Agent的token消耗和工具调用都有成本,如果任务本身的价值很低,用Agent可能不划算。

我见过一些团队为了"用上Agent"而用Agent,把本来用几行代码就能搞定的任务硬是包装成Agent任务,结果系统复杂度上去了,效果反而下降了。技术选型要服务于业务目标,而不是反过来。

在实际项目中,我通常会把任务拆分成两部分:确定性的部分用传统代码实现,不确定性的部分交给Agent。这样既能保证核心流程的稳定性,又能利用Agent的灵活性处理复杂情况。这种混合架构在目前的工程实践中,往往比纯Agent方案更可靠。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 4:00:43

多层结构瞬态响应仿真全解析:建模、时间步长与求解实战

1. 项目概述1.1 核心需求解析先说清楚这是个什么东西吧。你在做过程序仿真、设备热设计&#xff0c;或者单纯在学CFD类软件&#xff0c;迟早会碰上传热学仿真里的瞬态响应问题。普通稳态工况只告诉你系统最终会稳定在什么温度&#xff0c;但不回答“到底多久才能到”“中间会不…

作者头像 李华
网站建设 2026/10/10 4:00:26

低功耗手持设备电源管理实战:PCA9422与PIC32MX764F128L协同设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/10 4:00:11

12GB显卡如何跑125B大模型?Strata分层调度与量化压缩实战

1. 这个标题到底在讲什么第一次看到“12GB显卡跑125B参数模型”这个说法&#xff0c;我的第一反应是&#xff1a;这不可能。按照常规认知&#xff0c;125B参数的模型&#xff0c;光是权重加载&#xff0c;就算用4bit量化&#xff0c;也得占掉60GB以上的显存&#xff0c;12GB连零…

作者头像 李华
网站建设 2026/10/10 3:59:38

用AI搭建被动收入系统:从选品到自动交付的完整框架

1. 从“用工具”到“建系统”&#xff1a;被动收入的思维切换很多人第一次看到“用AI做被动收入”这个说法&#xff0c;脑子里蹦出来的画面大概是&#xff1a;打开对话框&#xff0c;敲几行字&#xff0c;然后钱就自动流进来了。我一开始也这么想过&#xff0c;甚至真的试过连续…

作者头像 李华
网站建设 2026/10/10 3:59:09

如何打造最优秀的AI辅助学习Skill:从设计到评估的完整指南

1. 拆解“最优秀AI辅助学习Skill”的真实含义1.1 这个标题到底在说什么“挑战成为最优秀的ai辅助学习skill”这个标题&#xff0c;乍一看像是一句口号&#xff0c;但它背后其实藏着一个非常具体的产品思路&#xff1a;把AI辅助学习这件事&#xff0c;从“一个万能聊天窗口”收缩…

作者头像 李华
网站建设 2026/10/10 3:59:01

频域分析实战:从FFT到振动故障诊断的关键技术解析

做振动测试那阵子&#xff0c;我经常碰到一种情况&#xff1a;时域波形图里信号乱成一团&#xff0c;忙活半天也看不出设备到底哪里不对劲。后来习惯了把信号丢到频域里看&#xff0c;几秒就能锁定问题来源。这就是频域分析的价值所在——把时间轴上的复杂波形拆解成不同频率的…

作者头像 李华