通用智能的本质是适应:构建可动态调度工具的AI Agent框架
2026/9/2 11:57:46 网站建设 项目流程

大家在业务系统里做智能化改造时,是不是经常遇到这种情况:需求方一开始说“帮我写一套工单自动分类逻辑”,你老老实实把分类规则、关键词表、优先级枚举全写死在代码里;上线没两周,业务又加了新的工单类型,甚至出现了“存量客户投诉转营销线索”这种跨域任务,你只能连夜改规则、加判断、发版上线。

这个问题的根源,不在于你少写了某个分支,而在于整个系统把“能力”预设得太死。规则引擎、状态机、流程编排都是典型的“预设能力”——它们适合范围明确、边界固定的场景。可一旦任务空间是开放的,预设方案的维护成本就会指数级上升。这也是为什么业内讨论“通用智能”时,越来越多人认可一个判断:通用智能的本质是适应,而非预设能力

这篇文章不打算做纯理论辨析,而是围绕这个观点,从 AI Agent(智能体)开发的角度完整拆解:为什么说“适应”才是通用性的关键,以及如何在工程上落地一套能动态调度工具、记忆上下文、在运行中自我修正的小型 Agent 框架。适合正在做 LLM 应用、Agent 编排、自动化流程的开发者,也适合想理解“通用智能”技术含义的产品和技术负责人。读完你可以直接照着把代码跑起来,并在自己的项目中复用这套思路。

1. 通用智能是什么?为什么“适应”比“预设”更关键

1.1 从“预设能力”说起

先给“预设能力”下一个通俗定义:所谓预设能力,就是把完成某个任务所需的步骤、规则、分支、参数,在系统设计阶段就固定下来。典型例子包括:

  • 传统规则引擎:if order.amount > 10000 then level = 'VIP'
  • 有状态工作流:状态机里定义好所有节点和转移条件。
  • 传统的意图识别 + 槽位填充:只识别预先定义好的查天气定闹钟等意图,超出范围就回退到兜底话术。

这种方式有两个明显优点:行为可预期、排错容易。但它也有一个致命短板——任务空间一旦扩大,规则数量会爆炸。100种规则还算可控,10000种规则就是维护灾难。更麻烦的是,规则之间还会互相干扰,很多团队最后改到一个规则就影响另一个规则,只能重构。

1.2 “适应”在 AI 系统中的含义

“适应”则不同。它不是提前写好每一个任务的实现步骤,而是让系统具备下面三种底层能力:

  1. 感知变化:能从输入中识别当前任务是什么、需要哪些资源、处于什么上下文。
  2. 动态决策:根据感知到的信息,在运行时选择工具、方法、执行顺序,而不是走固定分支。
  3. 反馈修正:执行后根据结果和错误信息调整策略,直到任务完成或被安全终止。

用一个形象的对比:预设能力的系统像一本印刷好的旅行手册,只覆盖写过的路线;适应能力的系统像一个会看地图、会问路、会根据天气调整行程的旅行者。后者不会被困在一本手册里。

1.3 对开发者意味着什么

当我们在 LLM 时代谈“通用智能”,并不是让每个团队去训练一个通用人工智能模型,而是指:上层系统设计应该具备开放性。具体到工程上,至少有三点转变:

  • 从写死规则,转向让模型理解任务并组合工具;
  • 从固定流程,转向“观察-决策-行动-反思”的循环;
  • 从一次性调用,转向带记忆和闭环反馈的多轮执行。

这也是后面几节要做的:用一个可运行的 Agent 示例,把这套“适应”能力落到代码上。

2. 环境准备与系统设计

2.1 开发环境与依赖

本文示例使用 Python 3.10 及以上版本,核心逻辑不依赖任何重型框架。为了让读者在没有大模型 API Key 的情况下也能端到端运行,我实现了两种推理后端:

  • StubLLM:本地模拟后端,用规则逻辑代替大模型,方便本机验证整体 Agent 流程。
  • OpenAICompatLLM:兼容 OpenAI Chat Completions 接口的后端,可替换为任意兼容该协议的模型服务,生产环境推荐使用。

依赖方面,示例尽量使用 Python 标准库,只有OpenAICompatLLM需要额外安装openai库和配置环境变量。你完全可以直接复制项目跑起来,再按需接入真实模型。

2.2 系统总体架构

本文将实现一个小型 Agent 框架,由五个核心模块组成:

模块职责
Tool定义工具的统一接口,包括名称、描述、参数、执行方法
ToolRegistry工具注册表,支持运行时动态注册、查找工具
Memory会话记忆,保存系统提示、历史消息、观察结果
LLMBackend模型推理后端,负责把 Agent 状态转成决策
Agent主循环,串联感知、决策、行动、观察和反思

整体执行流程如下:

  1. 接收用户任务;
  2. 从工具注册表加载可用工具的描述;
  3. 将任务、工具信息、历史记录一起交给 LLM 后端;
  4. LLM 返回决策 JSON,包含thoughtaction
  5. Agent 执行对应工具,得到观察结果;
  6. 把观察结果追加进内存,进入下一轮;
  7. 当 LLM 返回finish时结束循环,输出最终回答。

2.3 示例项目结构

agent-demo/ ├── main.py # 启动入口,演示完整流程 ├── agent/ │ ├── __init__.py │ ├── core.py # Agent 主类 │ ├── memory.py # 会话记忆 │ ├── tools.py # 工具抽象类和注册表 │ ├── llm.py # 推理后端(Stub + OpenAI兼容) │ └── system_prompt.py # 系统提示词模板 └── tools/ ├── __init__.py ├── time_tool.py # 示例工具:获取当前时间 ├── calculator.py # 示例工具:四则运算 └── file_reader.py # 示例工具:安全读取白名单目录文件

3. 核心原理拆解:Agent 如何实现“适应”

在写完整代码前,先把四个关键机制讲透。这不只是为跑通示例,更是为了让你在自己的项目里能举一反三。

3.1 动态工具发现与注册

传统程序的“功能”往往写死在方法里,调用关系是编译期确定的。而 Agent 的核心变化是:工具的描述以结构化文本形式暴露给模型,模型在运行时决定调用哪个工具。也就是说,“有什么能力”和“用哪个能力”被解耦了。

ToolRegistry 在启动时加载所有工具,并将每个工具的namedescriptionparameters格式化成模型能读懂的列表。模型不是从代码里找到某个函数,而是从上下文里的工具描述中作出选择。

更关键的是,ToolRegistry 支持运行时注册。比如项目启动后加载了一个tools/extra.py,其中定义了一个新工具,Agent 下一轮决策时就能感知到它。这就实现了“系统在运行中长出新的能力”——这是“适应”在工程上最直观的体现。

3.2 基于任务意图的推理循环

Agent 不是调用一次模型就返回,而是走一个循环:

  • Thought:模型先思考当前任务处于什么阶段,距离完成还差什么;
  • Action:选择要调用的工具名和参数;
  • Observation:工具执行结果返回给模型;
  • Repeat:直到模型判定任务完成,输出最终答案。

这个循环把一次性对话变成了持续的问题求解过程。任务越复杂,循环轮数越多。实现时需要注意给循环设置最大轮数,防止模型陷入死循环。

3.3 记忆与上下文管理

记忆对“适应”至关重要。没有记忆的 Agent,每一轮都像失忆的人重新开始,无法利用上一轮的观察结果。

Memory 模块保存两类信息:用户的原始请求、以及 Agent 每一步产生的thought/action/observation。每次请求模型时,把最近的记忆拼进上下文。还要设置窗口上限,避免上下文无限膨胀——这个问题在小模型或长任务场景尤其明显。

3.4 反思与纠错机制

“适应”的另一个关键点是容错。真实环境中,模型可能传错参数、调用不存在的工具、返回不合法 JSON。Agent 需要有明确的反思机制:

  • 如果工具调用报错,把错误信息作为 Observation 塞回上下文;
  • 如果模型返回了未注册的工具,尝试纠正为相近工具名;
  • 如果连续多轮失败,终止循环并给出可读的失败原因。

这个“错误也是一种观察结果”的思路,是 Agent 和普通程序最大的差异。普通程序报错就中断,Agent 可以把报错当作下次决策的输入。

4. 完整实战:实现一个可适应新任务的 Agent

接下来我们用代码把上面的设计落地。为了让代码可以直接运行,StubLLM会通过关键词识别模拟模型决策,OpenAICompatLLM则为真实环境预留了接入点。

4.1 定义工具接口与注册表

文件路径:agent/tools.py

# agent/tools.py from abc import ABC, abstractmethod from typing import Any, Dict, List, Optional class Tool(ABC): """所有工具必须继承 Tool,并实现 name/description/parameters/execute。""" @property @abstractmethod def name(self) -> str: """工具名称,模型通过该名称调用工具。""" @property @abstractmethod def description(self) -> str: """工具功能描述,模型会依据描述做决策,要写清楚用途和边界。""" @property def parameters(self) -> Dict[str, Any]: """参数 JSON Schema,用于描述工具需要哪些参数。""" return {"type": "object", "properties": {}} @abstractmethod def execute(self, **kwargs: Any) -> str: """执行工具,入参由模型解析后传入。返回结果必须是字符串。""" class ToolRegistry: """工具注册表,支持运行时动态注册、查找和描述格式化。""" def __init__(self) -> None: self._tools: Dict[str, Tool] = {} def register(self, tool: Tool) -> None: if tool.name in self._tools: raise ValueError(f"工具 {tool.name} 已注册") self._tools[tool.name] = tool print(f"[ToolRegistry] 注册工具: {tool.name}") def unregister(self, tool_name: str) -> None: if tool_name not in self._tools: raise KeyError(f"工具 {tool_name} 不存在") del self._tools[tool_name] print(f"[ToolRegistry] 注销工具: {tool_name}") def get(self, tool_name: str) -> Optional[Tool]: return self._tools.get(tool_name) def list_tools(self) -> List[str]: return list(self._tools.keys()) def describe_tools(self) -> str: lines = [] for tool in self._tools.values(): lines.append( f"- {tool.name}: {tool.description} | 参数: {tool.parameters}" ) return "\n".join(lines)

这里要解释几个细节:

  • Tool是抽象基类,所有业务功能都要实现成 Tool 子类。这样 Agent 主循环不需要关心“计算器怎么算”“文件怎么读”,只关心工具名和参数。
  • describe_tools()会把所有工具的描述格式化成一段文本喂给模型。这也是“适应”得以实现的基础:模型通过描述而不是硬编码调用关系来了解系统能力。
  • 注册表用字典保存工具,名字不能重复。运行时注册新工具是很自然的事,只要调用register()即可。

4.2 编写三个示例工具

文件路径:tools/time_tool.py

# tools/time_tool.py from datetime import datetime from typing import Any, Dict from agent.tools import Tool class GetCurrentTimeTool(Tool): @property def name(self) -> str: return "get_current_time" @property def description(self) -> str: return "获取当前系统时间,返回格式为 YYYY-MM-DD HH:MM:SS" @property def parameters(self) -> Dict[str, Any]: return {"type": "object", "properties": {}} def execute(self, **kwargs: Any) -> str: return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

文件路径:tools/calculator.py

# tools/calculator.py from typing import Any, Dict from agent.tools import Tool class CalculatorTool(Tool): @property def name(self) -> str: return "calculator" @property def description(self) -> str: return "执行四则运算,支持加减乘除。传入表达式,例如 '1 + 2 * 3'" @property def parameters(self) -> Dict[str, Any]: return { "type": "object", "properties": { "expression": { "type": "string", "description": "待计算的数学表达式", } }, "required": ["expression"], } def execute(self, **kwargs: Any) -> str: expression = kwargs.get("expression", "") # 出于演示目的使用 eval,生产环境必须使用安全的表达式求值库 try: result = eval(expression, {"__builtins__": {}}, {}) return str(result) except Exception as e: return f"表达式计算失败: {e}"

这里特别说明:eval只在本地演示中使用,真实项目请引入astevalsimpleeval这类安全求值库,否则会引入代码执行漏洞。安全边界会在第 6 节详细讲。

文件路径:tools/file_reader.py

# tools/file_reader.py import os from pathlib import Path from typing import Any, Dict from agent.tools import Tool class SafeFileReaderTool(Tool): """只能读取白名单目录下的文件,防止路径穿越。""" ALLOWED_DIR = Path("./data") @property def name(self) -> str: return "read_file" @property def description(self) -> str: return "读取白名单目录 data 下的文本文件内容" @property def parameters(self) -> Dict[str, Any]: return { "type": "object", "properties": { "filename": { "type": "string", "description": "相对 data 目录的文件名", } }, "required": ["filename"], } def execute(self, **kwargs: Any) -> str: filename = kwargs.get("filename", "") base = self.ALLOWED_DIR.resolve() target = (base / filename).resolve() # 校验目标路径是否还在白名单目录内 if not str(target).startswith(str(base)): return "错误:不允许访问白名单目录以外的文件" if not target.exists() or not target.is_file(): return f"错误:文件 {filename} 不存在" try: return target.read_text(encoding="utf-8") except Exception as e: return f"读取文件失败: {e}"

SafeFileReaderTool是一个很好的例子:AI 的能力边界必须通过代码硬性约束。模型可以建议读哪个文件,但“能不能读”由工具自己决定,而不是模型决定。

4.3 实现会话记忆与推理后端

文件路径:agent/memory.py

# agent/memory.py from typing import List, Dict, Any class ConversationMemory: """保存多轮对话和观察结果,并限制最大窗口长度。""" def __init__(self, max_messages: int = 20) -> None: self.messages: List[Dict[str, str]] = [] self.max_messages = max_messages def add_user(self, content: str) -> None: self.messages.append({"role": "user", "content": content}) def add_system(self, content: str) -> None: self.messages.append({"role": "system", "content": content}) def add_assistant(self, content: str) -> None: self.messages.append({"role": "assistant", "content": content}) def add_observation(self, content: str) -> None: self.messages.append({"role": "user", "content": f"[观察结果] {content}"}) def trim(self) -> None: # 保留最近的 max_messages 条消息 if len(self.messages) > self.max_messages: self.messages = self.messages[-self.max_messages:] def to_openai_messages(self) -> List[Dict[str, str]]: self.trim() return self.messages

记忆模块需要注意两点:

  • 观察结果不单独设计一个角色,而是放进user消息,并加了[观察结果]前缀。这样对主流 Chat 模型来说语义更兼容。
  • trim()通过截断历史控制上下文长度,避免超长。实际项目里可以做摘要压缩或向量检索,但那是另一个层面的优化。

文件路径:agent/llm.py

# agent/llm.py import json import os import re from abc import ABC, abstractmethod from typing import Any, Dict, List class LLMBackend(ABC): """模型推理后端抽象类。""" @abstractmethod def chat(self, messages: List[Dict[str, str]]) -> str: """输入对话消息,返回模型输出文本。""" class StubLLM(LLMBackend): """ 本地模拟模型后端:通过关键词匹配返回格式化的 JSON 决策。 仅用于演示 Agent 循环,实际项目请接入真实模型。 """ def chat(self, messages: List[Dict[str, str]]) -> str: last_user = "" for message in reversed(messages): if message["role"] == "user": last_user = message["content"] break text = last_user.lower() if "现在几点" in text or "当前时间" in text: return json.dumps( { "thought": "用户想获取当前时间,可以使用时间工具。", "action": {"name": "get_current_time", "args": {}}, }, ensure_ascii=False, ) if "计算" in text: match = re.search(r"计算\s*([\d\s+\-*/().]+)", text) expression = match.group(1).strip() if match else "1+1" return json.dumps( { "thought": "用户需要做数学计算,调用计算器工具。", "action": { "name": "calculator", "args": {"expression": expression}, }, }, ensure_ascii=False, ) if "读取" in text: filename_match = re.search(r"读取\s*([^\s]+)", text) filename = filename_match.group(1) if filename_match else "demo.txt" return json.dumps( { "thought": "用户需要读取文件内容,调用文件读取工具。", "action": { "name": "read_file", "args": {"filename": filename}, }, }, ensure_ascii=False, ) return json.dumps( { "thought": "任务无法通过现有工具完成,或者用户请求与工具集不匹配。", "action": {"name": "__finish__", "args": {}}, "answer": "抱歉,我没有找到能完成该任务的工具。", }, ensure_ascii=False, ) class OpenAICompatLLM(LLMBackend): """ 兼容 OpenAI Chat Completions 协议的模型后端。 生产环境将 OPENAI_API_KEY 和 OPENAI_BASE_URL 配置为你的服务地址, 模型自动读取 OPENAI_MODEL_NAME 指定的模型名。 """ def __init__(self, model: str | None = None) -> None: api_key = os.getenv("OPENAI_API_KEY") if not api_key: raise ValueError("未配置 OPENAI_API_KEY 环境变量") try: from openai import OpenAI except ImportError: raise ImportError("请先安装 openai 库:pip install openai") self._client = OpenAI(api_key=api_key) self._model = model or os.getenv("OPENAI_MODEL_NAME", "gpt-4o-mini") def chat(self, messages: List[Dict[str, str]]) -> str: response = self._client.chat.completions.create( model=self._model, messages=messages, temperature=0.2, ) return response.choices[0].message.content or ""

StubLLM虽然很简陋,但它体现了 Agent 循环的关键:模型输出不是自由文本,而是结构化的{thought, action}JSON。真实模型中,我们通过提示词要求模型返回同样结构,再由 Agent 解析。这样,Stub 和真实模型可以用同一套循环逻辑。

4.4 实现 Agent 主循环与系统提示词

文件路径:agent/core.py

# agent/core.py import json from typing import Any, Dict, List from agent.memory import ConversationMemory from agent.tools import ToolRegistry class Agent: def __init__( self, registry: ToolRegistry, llm, memory: ConversationMemory | None = None, max_steps: int = 5, system_prompt: str = "", ) -> None: self.registry = registry self.llm = llm self.memory = memory or ConversationMemory() self.max_steps = max_steps self.system_prompt = system_prompt def run(self, user_input: str) -> str: self.memory.add_user(user_input) step = 0 while step < self.max_steps: step += 1 print(f"\n===== Step {step} =====") tools_desc = self.registry.describe_tools() system_message = self.system_prompt.replace( "{{tools}}", tools_desc ) messages = [ {"role": "system", "content": system_message} ] + self.memory.to_openai_messages() try: raw_response = self.llm.chat(messages) decision = self._parse_decision(raw_response) except Exception as e: self.memory.add_observation(f"模型决策解析失败: {e}") continue thought = decision.get("thought", "") print(f"[Thought] {thought}") action = decision.get("action", {}) action_name = action.get("name", "") action_args = action.get("args", {}) if action_name == "__finish__": answer = decision.get("answer", "任务已完成。") self.memory.add_assistant(answer) return answer tool = self.registry.get(action_name) if tool is None: obs = f"错误:工具 {action_name} 不存在。当前可用工具: {', '.join(self.registry.list_tools())}" print(f"[Observation] {obs}") self.memory.add_observation(obs) continue try: result = tool.execute(**action_args) except Exception as e: result = f"工具 {action_name} 执行异常: {e}" print(f"[Action] {action_name}({action_args})") print(f"[Observation] {result}") self.memory.add_observation(result) return "已达到最大执行轮数,任务未能完成。请简化任务或增加可用的工具。" def _parse_decision(self, raw: str) -> Dict[str, Any]: """解析模型返回文本,兼容从 ```json 代码块中提取 JSON。""" text = raw.strip() if "```json" in text: start = text.find("```json") + 7 end = text.find("```", start) text = text[start:end].strip() elif "```" in text: start = text.find("```") + 3 end = text.find("```", start) text = text[start:end].strip() return json.loads(text)

Agent.run()是整个框架的核心,它把第 3 节讲的推理循环变成了真实代码。每一轮决策都基于系统提示词、历史记忆和工具描述,工具执行结果无论成功失败都会被记录为观察结果,供下一轮决策参考。这就是 Agent 能“适应”变化的直接原因。

文件路径:agent/system_prompt.py

# agent/system_prompt.py SYSTEM_PROMPT = """你是一个运行在安全沙箱中的通用智能助手。你要根据用户请求和可用工具,自主完成推理和行动。 你可以使用以下工具: {{tools}} 要求: 1. 每次只输出一个 JSON 对象,不要输出多余文字。 2. JSON 格式必须为: {"thought": "你当前的思考", "action": {"name": "工具名", "args": {"参数名": "参数值"}}} 3. 当你认为任务已经完成,或者现有工具无法完成用户请求时,输出: {"thought": "总结", "action": {"name": "__finish__", "args": {}}, "answer": "给用户的最终回答"} 4. 调用工具前,必须确认该工具确实已在可用工具列表中。不要臆造不存在的工具。 5. 如果工具执行报错,根据错误信息调整参数,最多重试两次。 6. 禁止调用不在列表中的工具,禁止访问未授权目录或执行危险操作。 7. 如果连续多轮没有进展,请尽快输出 finish,并向用户说明原因。 请确保行动可观测、可回滚,任何外部副作用操作都必须在执行前充分说明。 """

这个系统提示词可以看作“通用安全的智能体的系统提示词”的最小模板。它强调了三件事:工具白名单、结构化输出、失败兜底。真实项目中你还需要加入敏感信息过滤、数据脱敏、审计日志等约束。

4.5 编写启动入口并运行

文件路径:main.py

# main.py from agent.core import Agent from agent.memory import ConversationMemory from agent.llm import StubLLM from agent.system_prompt import SYSTEM_PROMPT from agent.tools import ToolRegistry from tools.calculator import CalculatorTool from tools.file_reader import SafeFileReaderTool from tools.time_tool import GetCurrentTimeTool def build_agent() -> Agent: registry = ToolRegistry() registry.register(GetCurrentTimeTool()) registry.register(CalculatorTool()) registry.register(SafeFileReaderTool()) # 默认使用 StubLLM 本地运行;配置好 OPENAI_API_KEY 后可直接切换为: # llm = OpenAICompatLLM() llm = StubLLM() memory = ConversationMemory(max_messages=20) return Agent( registry=registry, llm=llm, memory=memory, max_steps=5, system_prompt=SYSTEM_PROMPT, ) if __name__ == "__main__": agent = build_agent() tasks = [ "现在几点?", "帮我计算 (12 + 34) * 2", "读取 data/demo.txt 的内容", "帮我把房间温度调高一点", ] for task in tasks: print("\n======================") print(f"[用户请求] {task}") answer = agent.run(task) print(f"[Agent 回答] {answer}")

在项目根目录创建data/demo.txt,写入如下内容:

你好,这是 Agent 白名单目录中的示例文件。

运行命令:

python main.py

预期输出大致如下(为了排版做了简化):

====================== [用户请求] 现在几点? ===== Step 1 ===== [ToolRegistry] 注册工具: get_current_time ... [Thought] 用户想获取当前时间,可以使用时间工具。 [Action] get_current_time({}) [Observation] 2025-02-13 10:24:31 [Agent 回答] 任务已完成。

注意:由于main.py中先输出了注册日志,实际控制台会有多条注册日志。关键是最后三个任务都能被 Agent 正确识别并调用对应工具;第四个“调节温度”没有对应工具,Agent 会输出“抱歉”类回答。这就体现了 Agent 在开放任务下的“适应”边界:能做的调用工具完成,不能做的显式说明。

如果你配置了OPENAI_API_KEY和兼容服务地址,可以把llm = StubLLM()替换为llm = OpenAICompatLLM(),再用一句话测试切换效果:

export OPENAI_API_KEY=your_api_key export OPENAI_MODEL_NAME=gpt-4o-mini python main.py

真实模型能理解更复杂的自然语言任务,但 Agent 循环本身不需要改一行代码。这是接口抽象带来的好处。

5. 常见问题与排查思路

在实践这套 Agent 方案时,下面几个问题最容易遇到。整理成表格,方便快速定位。

问题现象常见原因解决思路
模型总是返回不存在的工具工具描述不清晰,或系统提示词未强调白名单约束检查describe_tools()输出,确保工具描述写清楚用途;在提示词中反复强调“只能使用列表中的工具”
Agent 陷入死循环,不断调用同一工具观察结果没有让模型意识到状态变化检查工具返回结果是否有足够信息;在提示词中要求模型总结进展;增加最大轮数限制
输出 JSON 解析失败模型返回了```json包裹的代码块,或者夹杂解释文字使用_parse_decision()中的兼容逻辑;在提示词中严格限定输出格式
文件读取工具被路径穿越对用户传入的路径未做白名单校验参考SafeFileReaderTool,用resolve()后对比基准目录
上下文窗口超限多轮循环后记忆消息过多缩小ConversationMemory.max_messages,或引入摘要压缩
StubLLM 能跑,换真实模型后效果变差真实模型对格式和工具描述更敏感优化系统提示词;给工具增加更完整的参数description;降低temperature至 0.2 以下

下面再展开三个高频场景。

5.1 模型“幻觉工具”问题

即使是 GPT-4 级别的模型,也可能在面对模糊工具名时“编造”出一个不存在的工具。比如项目里工具叫read_file,模型却输出readFilegetFileContent。这通常不是模型笨,而是工具名不符合常见命名习惯,或者系统提示词没有强调可用工具列表。

解决方案是双管齐下。第一,工具命名尽量短小、符合英语惯例。第二,在解析层做一次容错映射:如果模型输出的名称未注册,尝试在工具列表中找一个最相近的,比如忽略大小写和分隔符后再匹配。示例中ToolRegistry.get()是精确查找,真实项目可以扩展一个fuzzy_get()方法。

5.2 Agent 反复执行同一工具没有进展

假设模型已经调用了calculator,但结果是一次字符串拼接错误,下一轮模型可能又用同样参数再调一次。这种“原地打转”的本质是模型没有从错误中反思。

建议在三层加强:提示词层要求“如果同一工具连续两次报错,必须换一种策略”;代码层记录最近 N 轮 action 的签名,如果重复出现,直接给观察结果加上“该操作已失败过”的提示;控制层设置max_steps,保证系统不会无限消耗资源。

5.3 上下文窗口溢出

多轮工具调用会让Observation快速堆积,尤其是文件读取类工具返回大段文本时。此时简单粗暴地截断早期消息会丢失重要信息,更好的做法分两步:

  • 对单次 Observation 做截断,比如最多保留 1000 字符;
  • 对超过窗口的历史消息做摘要,用一句“用户之前请求过 X,已完成 tool Y,结果为 Z”替换整段历史。

这个摘要策略在很多生产级 Agent 中都很常见,既能压缩 token,又能保留关键上下文。

6. 工程最佳实践:安全、成本与可观测性

把这套 Agent 从 Demo 推向生产环境,有几个工程问题必须在设计阶段就考虑清楚。尤其当智能体拥有调用外部工具、读写文件、访问网络的能力时,“通用安全智能体”不是一句口号,而是一组硬约束。

6.1 安全边界与最小权限原则

Agent 的能力越强,安全边界越要收窄。建议从四个维度进行控制:

  1. 工具白名单:所有工具必须显式注册,Agent 不能动态创建工具。业务需要新增能力时,走代码评审和发布流程。
  2. 路径安全:文件读写工具必须做目录白名单校验。示例中的SafeFileReaderTool已经演示了resolve()+startswith()的校验方式,生产项目还要考虑软链接、权限位等因素。
  3. 命令执行风险:不要在生产环境中使用eval/exec执行模型生成的表达式。如果确实有计算需求,使用simpleevalasteval这类受限解释器,或者直接调用系统自带的安全计算库。
  4. 人工审核与回滚:涉及发消息、改配置、删数据等有副作用的行为,必须设计审批节点。工具执行前记录参数,执行后记录结果,便于审计和回滚。

6.2 成本控制与请求优化

LLM 调用按 token 计费,Agent 循环天然会放大调用次数。一个 5 步任务至少产生 5 次模型调用,每轮还要把所有历史记录重新发一遍。成本优化有几个抓手:

  • 精简系统提示词:删除与当前任务无关的规则描述,把工具描述控制在必要长度。
  • 压缩历史消息:每轮对话结束后,用摘要保留信息而不是全量保存。
  • 复用模型结果:对相似请求做缓存,比如天气查询、计算结果这类确定性输出,可以用 Redis 缓存减少模型调用。
  • 设置预算上限:记录每次任务的累计 token 数,达到阈值后强制终止并转人工。

建议给每次 Agent 运行打印 token 监控日志,方便后续优化。示例中的OpenAICompatLLM返回对象里本身包含 usage 信息,可以在这里采集。

6.3 日志、追踪与可观测性

Agent 的决策过程是非确定性的,同一句话可能走出不同路径。因此,必须把每一步的 thought、action、args、observation 全部记录下来,形成完整的 trace。

建议每条 trace 至少包含:

  • 会话 ID、用户 ID(脱敏后)
  • 用户原始输入
  • 每轮模型的 thought 和 action
  • 工具执行耗时和结果
  • 最终回答或失败原因

日志记录不仅是排错依据,也是后续优化系统提示词、调整模型参数的重要数据。很多团队踩坑后都感慨:没有 trace 的 Agent 就像一个黑盒,出了问题完全不知道是模型理解错、工具写错,还是上下文截断了。

6.4 系统提示词的设计原则

结合前文提到的“通用安全的智能体的系统提示词”,我总结四条设计原则,供你在实际项目中复用:

  1. 结构化输出优先:不要用自然语言描述期望输出格式,而是直接给出 JSON 模板,并要求模型“只输出 JSON,不要输出其他内容”。
  2. 边界描述明确:明确说“只能使用工具列表中的工具”“禁止访问未授权路径”,约束要具体,而不是笼统地写“请谨慎操作”。
  3. 给模型留出反思空间:要求模型在每轮行动前输出thought,这既是为了输出稳定,也是给后续排错留证据。
  4. 结束条件清晰:定义什么情况下必须结束循环,比如“任务完成”“连续两次相同错误”“用户要求停止”。没有清晰结束条件的 Agent 容易陷入无效循环。

7. 总结与下一步学习路线

这篇文章从“通用智能的本质是适应而非预设能力”的判断出发,完整实现了一个可运行的 Agent 示例。你现在应该已经掌握了几件事:

  • 预设能力和适应能力的核心区别在于任务空间是否开放;
  • Agent 通过工具注册、动态决策、记忆管理和纠错机制实现“适应”;
  • 一个最小可运行的 Agent 框架只需要工具注册表、记忆模块、模型后端和主循环四部分;
  • 生产环境必须在工具白名单、路径校验、成本控制、日志追踪等方面加固,才能真正做到“通用且安全”。

下一步建议你从两个方向继续深入。如果你关注模型侧,可以学习如何用函数调用(Function Calling)代替手工 JSON 解析,很多模型服务商都原生支持结构化工具调用,效果比提示词约束更稳定。如果你关注系统侧,可以把重点放在记忆压缩、多 Agent 协作和工具自动生成上——比如让 Agent 在运行中通过生成代码来创建新工具,这正是“适应能力”的进一步延伸。

当然,这一套代码并不是真正的 AGI,它也只是一个工程原型。但理解“适应而非预设”这个原则,会帮助你避免很多无效的堆规则式开发。下次再遇到“能不能给我加个新功能”的需求,你可以先想想:是继续往规则里加分支,还是把能力作为一种可配置、可组合、可动态调度的资源交给模型去编排。

如果这篇文章对你有帮助,建议收藏备用,也欢迎在评论区聊聊你在 Agent 开发中踩过的“预设能力”的坑。

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

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

立即咨询