现在做AI应用,最常遇到的一个尴尬是:模型聊得头头是道,可真让它干点活就抓瞎——让它查个订单状态,它只能回复“建议你登录后台查看”;让它定时汇总数据,它一概说“我无法主动执行”;更别提跨多个系统处理一次完整流程了。这就是我搭建Agent-Reach的原因:它不是又一个聊天机器人,而是一层专门解决“智能体触达能力”的基础设施。简单说,Agent-Reach 让 Agent 不再只是个“嘴上专家”,而能真真切切调数据库、发请求、写文件、开浏览器,把能触达的资源边界从对话窗口扩展到整个业务系统。这篇博文会把 Agent-Reach 的核心设计、关键实现、踩坑经验和可复现的代码一并拆开,适合正在做智能体落地的开发者、架构师,以及准备把 AI 接进现有系统又不知道怎么控制权限的人。
1. 为什么需要 Agent-Reach:智能体的“能力孤岛”问题
1.1 裸 Agent 的真实瓶颈:只会说话,不会办事
先抛开所有概念,回到最朴素的场景:你接一个 OpenAI 兼容接口或本地大模型,得到的默认能力是什么?是“文字接龙”。它可以把“请帮我写一封催款邮件”回答得滴水不漏,但如果任务是“请读取 /var/log/app.log 中今日的 ERROR 行数,并把结果写入数据库”,纯对话模型基本办不到——它没有访问文件系统的通道,没有数据库连接串,也没有执行代码的沙箱。
你可能会说,这种任务让普通程序写也不难,为什么非要 Agent?问题是真实业务里的任务往往是模糊的、多步的、跨系统的。比如“小王说他的订单一直显示已发货但没收到快递,你帮我查一下这个订单的物流状态,如果超过7天未签收就加急并通知仓储同事”。这种需求牵扯订单库、物流 API、通知服务三套系统,如果用硬编码写,每次换业务都要改代码;如果用裸模型跑,模型根本没有渠道拿到订单号和物流数据。于是就有了两边都难受的“能力孤岛”:系统有数据有接口,但 Agent 够不着;Agent 有理解力,但没有执行通道。
Agent-Reach 解决的就是这个“够不着”的问题。它的名字拆开就是Agent(智能体)+Reach(触达范围)——把智能体能够触达的资源边界,从“仅有对话”扩展到“任意注册过的工具”。同一个模型,绕过了 Agent-Reach,它只会空对空;接入 Agent-Reach 之后,它就能通过工具定义去操作真实世界。
1.2 设计思路:把“触达”变成一种基础设施
如果你看过一些开源 Agent 项目,会发现每个人都在自己的代码里写“if 用户说要查天气 then 调天气API”这种硬编码路由。Agent-Reach 换了种思路:不把工具调用写死在业务流程里,而是做成一个独立的、统一的能力触达层,跑在模型和业务系统之间。
打个比方,传统做法像是每个房间插座形状都不一样,每买一个新电器,都要单独改接线板;Agent-Reach 像是装上了一个“万能转换插座”。模型只需要用一种标准方式表达“我想读取订单号 XXX 的数据”,Agent-Reach 负责把这个意图翻译成具体工具调用,并解决权限、参数格式、结果返回、失败重试等一系列问题。开发者要做的,只是把系统里已有的 API、函数、数据库连接,包装成“工具”,然后注册进 Agent-Reach 的工具表里。
这样做有几个直接收益:
- 解耦:模型不需要感知每个系统的具体接口语法,只需理解工具描述;业务系统也不需要知道模型的存在,只被当作一个被调用方。
- 可扩展:新功能加入时,只需要增加一个新工具注册项,不用动 Agent 的对话逻辑。
- 可治理:调用了什么工具、参数是什么、成功还是失败、耗时多少,全部能记录成结构化日志。这在纯对话框架里几乎没有,但对生产系统极其重要。
从架构层级看,Agent-Reach 位于模型之上,但又独立于具体模型厂商。你可以接 OpenAI,也可以接本地 Qwen,甚至切换不同模型时,Agent-Reach 的工具定义和权限策略完全不用改。上层模型只是一个“意图理解引擎”,真正做事的是工具层。
1.3 与其他方案的关联:ReAct、Function Calling 和 MCP 各司其职
很多读者可能了解 Function Calling(函数调用)或 ReAct(Reasoning and Acting)范式,会问 Agent-Reach 是不是重复造轮子。这里要澄清:Agent-Reach 并不是要替代它们,而是在它们之上做一层工程化封装。
- 大模型自带的 Function Calling,解决的是“模型能够输出一个结构化的调用意图”,比如输出
{"name": "query_order", "arguments": {"order_id": "123"}}。但模型不知道这个函数存在吗?知道,因为你在请求里把函数列表传给了它。问题是函数列表一旦超过十几个,模型就容易选错或用错参数,而且没有权限控制、没有重试机制、没有限流。 - ReAct 是指模型自己推理“下一步该做什么”,再决定调用什么工具,它更像是思想框架,不面对工程问题。
- MCP(Model Context Protocol)这类标准化协议,定义的是模型与外部工具如何通信的“握手方式”。Agent-Reach 可以支持标准协议,同时也可以用自己的轻量工具模式,不一定要兼容所有协议。
我的理解是:Agent-Reach 是一个完整的工具治理层,它吸收 ReAct/Function Calling 的思路,替你去掉了 prompt 工程和函数定义手写的大量重复工作,并为它们补上了权限、审计、上下文管理、限流这类落地必须但模型公司不会给你的能力。所以它更像“智能体后端的后端”,而不是另一个模型调用方式。
2. 核心细节解析与实操要点
2.1 工具注册与发现机制:每个工具都主打一个“说明书”
在 Agent-Reach 里,“工具”是最小的能力单元。一个工具由四个东西组成:名称、用途描述、参数 Schema、实际执行函数。工具注册就是把这四者关联起来,放进一个工具登记表。
名称必须全局唯一,且要能一眼看出功能方向,比如query_order_status、send_dingtalk_message。描述是整个机制的灵魂,因为模型是靠描述来选择工具的。一段好的描述要写清楚:这个工具是什么、在什么场景下用、能返回什么、有什么限制。写不好描述的结果,就是模型在决策时自信地调到一个牛头不对马嘴的工具上。
参数 Schema 直接决定输入校验。建议遵循 JSON Schema 规范,每个参数要声明类型、是否必填、枚举范围、格式说明。工程师最容易忽略的是description字段,觉得只是给自己看的,但其实模型会依据字段描述来填参数。例如:
{ "name": "query_order_status", "description": "根据订单ID查询订单的当前状态和物流信息,只支持已支付订单,订单ID为6位数字", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单ID,例如 100023,必须是6位数字字符串", "pattern": "^\\d{6}$" } }, "required": ["order_id"] } }上面的描述里明确写了“订单ID为6位数字”,参数里再加正则校验,双保险。Tool 的实际执行函数就是一段普通 Python/Java 函数,里面想调用数据库、外部 HTTP 都行,唯一要求是遵循统一的输入和返回约定。我这边一般约定返回结构统一为:
{ "status": "success", "data": {}, "error": null, "trace_id": "xxxx" }这样 Agent-Reach 能统一解析结果,不管底层多少个工具,模型看到的都是相同格式的 JSON。
注册流程也简单:启动时扫描指定目录下所有 decorator 标记的函数,把它们的信息收集进工具表,生成一份全局工具清单。技术上是静态扫描注册表,不算新鲜,但这种“纯声明式”方式能让团队里任何人新增工具时不改脚手架,只写一个函数加一段描述即可。
2.2 意图感知与路由分发:让 Agent 准确找到“该用哪个工具”
工具数量少的时候,直接把全部工具定义塞给模型让它自己选就行。可一旦工具超过 15~20 个,模型的选择准确率会明显下降,经常出现“参数抄错”“把查询工具当成删除工具调”的场面。原因是模型在超长指令里注意力会被稀释。所以 Agent-Reach 做了一层预检索路由:收到用户请求后,先用一个轻量的嵌入式模型(或者简单的关键词匹配)算出应该调用的候选工具列表,只把前 3~5 个候选工具的“说明书”发给大模型,让后者做最终选择。这样既减少了 token 消耗,又降低了选择错误率。
预检索的原理不复杂:为每个工具的描述生成一个向量,存入内存索引或向量数据库。用户输入进来也向量化,计算余弦相似度,取出最相近的 TopK。如果场景只有几个工具,甚至可以按关键词匹配,例如包含“订单”“物流”这些词,就锁定订单工具。我实测下来,10 个以内工具时,静态描述直接全量送过去也可行;但到 30 个以上,预检索带来的准确率提升非常明显,建议所有人都加上。
路由分发还需要注意“歧义消解”。比如用户说“查一下王哥的合同”,他其实是想查附件里的 PDF,但工具列表里有合同库和人员库两个入口。遇到这种模糊意图,Agent-Reach 的策略是让大模型反问一次,而不是猜测。做法是:如果预检索出来的候选工具分值相差不超过 0.1,就触发“多候选请求澄清”,让 Agent 输出问题而不是调用。这一步能让后续参数填写精准很多,也避免误操作。
2.3 权限控制与安全边界:不该触达的坚决不触达
Agent 触达能力越大,风险也越大。Agent-Reach 在权限设计上借鉴了“最小权限原则”。每个 Agent 实例(或每次会话)都会绑定一个权限策略,策略里声明它可以使用哪些工具,以及每个工具的敏感级别。
敏感级别我分了三级:
- L1:只读查询类,比如查天气、查状态。允许自动执行。
- L2:会修改数据的,比如写数据库、发消息、创建订单。需要二次确认,或要求用户提供明显同意词。
- L3:高危操作,比如删除数据、转账、改配置。默认禁止,需要系统管理员手动审批。
实际实现时,Agent-Reach 维护一张工具与策略的映射表,路由分发之后、调用实际函数之前,先做策略判定。判断结果是“拒绝”则直接返回错误信息,并记录审计日志。关键点在于,这种判定不能依靠模型自觉,必须在代码层强制拦截,否则一个 prompt injection 就可能让 Agent 干坏事。
这里也涉及密钥管理。工具函数内要连接外部 API、数据库时,绝不能把密钥明文写进代码或发给模型。Agent-Reach 内部有一个密钥对账表:每个工具只存密钥的别名(如DB_PASSWORD_ALIAS),实际密钥放在环境变量或密钥管理服务中,由 Agent-Reach 在运行时注入到函数执行环境。模型永远看不到真实密钥,这样可以避免模型在输出中泄露敏感信息。
2.4 上下文管理与记忆压缩:别让工具结果撑爆对话窗口
工具调用一旦成功,返回结果会作为上下文重新发给模型,让它生成下一步动作。这个链路最大的坑是“上下文爆炸”。查一次订单返回 10KB 数据,模型很可能会忽略它,或者下一个工具调用被这些冗长结果挤掉了。更严重的是,模型有关键信息的注意力可能被淹没,导致业务判断失误。
Agent-Reach 的对策是“结果摘要与分层存储”。每个工具返回的原始数据不直接全文塞回语境,而是经过一层适配器:
- 如果返回数据小于某个阈值(比如 1000 字符),原样保留。
- 如果超过阈值,则提取关键摘要字段。例如查询订单,只保留
order_id, status, latest_logistics, timeout这几个字段,把物流轨迹的中间过程压缩成"count=8, first=xx, last=xx"。 - 如果摘要仍然太长,Agent-Reach 会把完整结果存入内存对象存储,并在上下文中只给出一个引用 ID 和“可检索”提示,让模型在需要细节时用专门工具去获取。
上下文预算我一般这样分配:按模型最大 token 数的 20% 做保守上限。例如 8K 上下文的模型,系统 prompt 占 1K,用户对话历史留 4K,工具调用的结果和中间推理最多只能占 3K。如果超过,就触发压缩,把最旧的对话历史转录成摘要。
这一步不仅省 token 费,而且能提高模型准确度。实测中,上下文里非核心信息减少一半后,工具选择准确率能提升 8~15 个百分点。
3. 实操过程与核心环节实现
3.1 环境准备与基础框架搭建
前面讲了不少概念,现在进入能跑的部分。我最常用来搭 Agent-Reach 骨架的组合是 Python 3.10 + AnyIO + FastAPI 加一个简单的工具注册器。之所以不用重型的 Agent 框架,是希望保持逻辑透明,好控制。先看一个最小骨架的目录:
agent_reach_demo/ ├── main.py # FastAPI 入口,同时初始化 Agent 服务 ├── tools/ │ ├── __init__.py │ ├── registry.py # 工具注册器 │ ├── order_tool.py # 订单查询工具 │ └── notify_tool.py # 通知发送工具 ├── core/ │ ├── policy.py # 权限策略判定 │ └── context.py # 上下文压缩与摘要 └── config.py # 全局配置(工具保存路径、模型参数等)注册器的核心逻辑其实很短,本质就是一个装饰器加一个类变量:
# tools/registry.py import inspect from typing import Callable, Dict class ToolRegistry: _registry: Dict[str, dict] = {} @classmethod def register(cls, name, description, parameters, level="L1"): def decorator(func: Callable): cls._registry[name] = { "name": name, "description": description, "parameters": parameters, "level": level, "func": func, } return func return decorator @classmethod def get_tool_metadata(cls): return [ {k: v for k, v in tool.items() if k != "func"} for tool in cls._registry.values() ] @classmethod def invoke(cls, name, arguments): tool = cls._registry.get(name) if not tool: raise ValueError(f"tool {name} not found") return tool["func"](**arguments)这里没有过度设计。工具描述、参数 Schema 都直接写在装饰器参数里,读起来就是一目了然。FastAPI 只负责暴露两个端点:一个/chat用于接收用户请求,一个/tools用于查看注册的工具列表,方便调试。
3.2 开发一个自定义工具:以“查询订单状态”为例
我拿一个电商里最常见的场景来做示例:查询订单状态。先写工具函数:
# tools/order_tool.py from tools.registry import ToolRegistry import random @ToolRegistry.register( name="query_order_status", description="根据订单ID查询订单当前状态和最新物流信息,订单ID为6位数字字符串", parameters={ "type": "object", "properties": { "order_id": { "type": "string", "description": "订单ID,例如 100023,6位数字字符串", "pattern": "^\\d{6}$" } }, "required": ["order_id"] }, level="L1" ) def query_order_status(order_id: str): # 真实项目里这里会查 MySQL 或缓存,示例用本地字典模拟 fake_db = { "100023": {"status": "已发货", "logistics": "中通快递 7561", "days_since_ship": 9}, "100024": {"status": "待发货", "logistics": "", "days_since_ship": 0}, } order = fake_db.get(order_id) if not order: return {"status": "failed", "error": "订单不存在", "data": None} # 注意:返回数据里可能有关键字段,比如超时天数,后面上下文压缩会用到 return {"status": "success", "data": order}然后写一个通知工具:
# tools/notify_tool.py from tools.registry import ToolRegistry @ToolRegistry.register( name="send_work_notification", description="向指定员工发送工作通知消息,用于提醒、告警。需要员工姓名和消息内容。", parameters={ "type": "object", "properties": { "employee_name": {"type": "string", "description": "员工姓名"}, "message": {"type": "string", "description": "通知的具体内容"} }, "required": ["employee_name", "message"] }, level="L2" # 写操作,需要二次确认 ) def send_work_notification(employee_name: str, message: str): # 真实项目里会调 IM 接口或钉钉 webhook return {"status": "success", "data": {"sent_to": employee_name, "msg_len": len(message)}}到这里,Agent-Reach 的工具层就完成了。注意一点:查询工具是 L1,可以自动执行;通知工具是 L2,必须在策略层触发二次确认。这个权限区分不是摆设,后面在 Agent 循环里必须落代码。
3.3 接入多个数据源与告警场景:从“能用”到“好用”
有了两个工具之后,把它们接入 Agent 循环。这里我用一个非常直接的模式:接收到用户请求 -> 预检索候选工具 -> 让模型生成结构化调用 -> 权限判定 -> 执行 -> 摘要 -> 再让模型继续,直到它认为任务完成。
先看预检索,如果工具不多,可以直接全部列出,但我想演示预检索代码:
# core/router.py import numpy as np from tools.registry import ToolRegistry # 简单起见用关键词匹配代替向量库;真实场景建议使用小型 embedding 模型 def prefilter_tools(user_message: str, top_k: int = 3): scores = [] for tool in ToolRegistry.get_tool_metadata(): score = 0 name_and_desc = tool["name"] + " " + tool["description"] for kw in ["订单", "物流", "查询", "通知", "消息", "时间", "状态"]: if kw in name_and_desc and kw in user_message: score += 1 scores.append(score) ranked = sorted(zip(ToolRegistry.get_tool_metadata(), scores), key=lambda x: -x[1]) return [item[0]["name"] for item in ranked[:top_k] if item[1] > 0]这里的词表写得很粗,真实方案可以换成 embedding。核心思想是一样的:缩小模型决策范围。
接下来是核心的 Agent 循环主体。为了省 token,我用一个函数来完成“生成调用 -> 执行 -> 反馈”的循环:
# core/agent_loop.py import json from tools.registry import ToolRegistry from core.policy import Policy from core.context import summarize_result def run_agent(user_message: str, model_call_fn, policy: Policy, max_steps=5): # 预检索,得到候选工具清单 candidate_tools = prefilter_tools(user_message, top_k=3) # 给模型系统提示,仅列出候选工具,并强制要求输出 JSON 格式的调用意图 tool_meta = [t for t in ToolRegistry.get_tool_metadata() if t["name"] in candidate_tools] system_prompt = ( "你是智能体执行引擎,只负责根据用户请求选择工具并填参数。" "必须输出严格 JSON,格式为 {'tool': '工具名', 'arguments': {...}}。" "如果没有合适的工具,输出 {'tool': 'none', 'reason': '解释'}。\n" "可调用工具如下:\n" + json.dumps(tool_meta, ensure_ascii=False) ) history = [{"role": "user", "content": user_message}] for _ in range(max_steps): response = model_call_fn(system_prompt, history) intent = json.loads(response) if intent.get("tool") == "none": return {"finish": True, "answer": intent.get("reason"), "history": history} tool_name = intent["tool"] args = intent["arguments"] # 权限判定 if not policy.check(tool_name, args): return {"finish": True, "answer": "该操作没有权限,已拒绝"} # 调用工具 try: result = ToolRegistry.invoke(tool_name, args) except Exception as e: return {"finish": True, "answer": f"工具调用失败: {str(e)}"} # 摘要和压缩 short_result = summarize_result(result) history.append({"role": "assistant", "content": f"工具 {tool_name} 返回:{json.dumps(short_result, ensure_ascii=False)}"}) # 让模型决定下一步 return {"finish": False, "answer": "步骤超限,已自动终止", "history": history}Policy 类最简单版本:
# core/policy.py class Policy: def __init__(self, allowed_tools: set, sensitive_tools: set): self.allowed_tools = allowed_tools self.sensitive_tools = sensitive_tools def check(self, tool_name: str, args: dict) -> bool: if tool_name not in self.allowed_tools: return False if tool_name in self.sensitive_tools: # L2 工具的二次确认,示例里简单模拟 print(f"[需要确认] 工具 {tool_name} 参数 {args}") confirm = input("确认执行? (y/n): ") return confirm.strip().lower() == "y" return True这样就把权限判定硬编码在调用链路上,不依赖模型自觉。实际生产里二次确认会做成一个独立 HTTP 接口,由用户在网页上点确认,而不是控制台。
有了这些,你可以在 FastAPI 里暴露/chat:
# main.py from fastapi import FastAPI from pydantic import BaseModel from core.agent_loop import run_agent app = FastAPI() class ChatRequest(BaseModel): message: str def model_call_fn(system_prompt, history): # 这里接入你选的模型,例如 OpenAI 兼容接口 # 返回一个字符串 JSON return '{"tool": "query_order_status", "arguments": {"order_id": "100023"}}' @app.post("/chat") def chat(req: ChatRequest): policy = Policy(allowed_tools={"query_order_status", "send_work_notification"}, sensitive_tools={"send_work_notification"}) result = run_agent(req.message, model_call_fn, policy) return result当然,上面model_call_fn是假的,只是为了演示链路。真实场景要接入一个会生成结构化 JSON 的模型,需要设置response_format={"type": "json_object"}或者在 prompt 里强约束。别用没有 JSON 能力的模型硬来,不然解析会疯掉。
3.4 日志追踪与可观测性
Agent-Reach 这类系统的难点在于“黑盒”:一个问题出现了,到底是模型决策错了、还是工具执行错了、还是权限拒绝了?没日志寸步难行。所以从第一版开始就在核心执行链路埋点。我在run_agent里加了一个简单的追踪结构:
# core/trace.py import time, uuid def make_tracer(): trace_id = uuid.uuid4().hex[:12] events = [] def record(event_type, payload): events.append({ "ts": time.time(), "trace_id": trace_id, "event_type": event_type, "payload": payload, }) return trace_id, record在run_agent循环的每个关键节点打点:预检索候选结果、模型返回的意图、权限判定结果、工具执行耗时、上下文压缩前后长度。最后返回给用户时,带上trace_id;日志可以写到 JSON Lines 文件,也可以推到 Loki。有了这些数据,后续做失败分析、令牌用量统计就都有依据了。
4. 常见问题与排查技巧实录
4.1 工具调用不生效:一直返回错误怎么办
这是接入初期最烦的问题。你定义了工具,模型也输出了调用意图,但执行时报参数错误,或返回的数据看不懂。我碰到的典型原因和排查方法依次如下:
- 参数 Schema 写错,导致模型填出的参数类型不对。例如把
order_id定义成 integer,模型输出数字100023,但实际工具函数需要字符串,Python 不会自动转换,导致 SQL 或 HTTP 拼接错误。解决:给每个参数加严格的type和pattern,并在注册器里加一个类型校验器,提前拒绝不符合 Schema 的调用。 - 工具描述过于模糊。比如只写“查询订单”,模型不知道是查询订单状态还是查询订单金额,可能调用成别的工具。解决:回调
get_tool_metadata看看模型实际收到底层描述是什么,逐字优化工具说明,做到“用户说什么关键词,描述里就要有什么关键词”。 - 上下文里结果太大,模型忽略或胡诌。前面提到过,查询返回一大块 JSON,模型看到太多信息后可能会忘记。解决:压缩摘要、保留关键字段、必要时让模型专门调用“获取完整详情”工具。
4.2 模型“幻觉工具”:没注册却硬编代码
更大的坑是模型不按工具调用来,而是在回复里自己编一个get_order_info之类不存在的方法名,尤其是你喂给它的工具清单不全,或者提示词里没强调“只能调用我列出的工具”。我的处理办法是在 system prompt 里加“强制约束”,并且在解析 JSON 前校验工具名:
def safe_invoke(tool_name, args): available = {t["name"] for t in ToolRegistry.get_tool_metadata()} if tool_name not in available: raise ValueError(f"invalid tool name: {tool_name}, available tools: {available}") return ToolRegistry.invoke(tool_name, args)还有一个技巧:把候选工具清单用 Markdown 表格放在 system prompt 最前面,表格列只保留“工具名 | 用途 | 关键参数”,不要放无关内容。模型对列表开头的工具往往更敏感,幻觉率会下降不少。
4.3 并发与性能问题:多个 Agent 抢同一工具
当 Agent-Reach 同时处理多个会话时,会遇到工具函数非线程安全、外部 API 限流、共享数据库连接池被耗尽等问题。最直接的方法是加一层“并发上限控制”。我自己用的简单工具是 Python 的asyncio.Semaphore,为每个敏感工具设置最大并发数:
# core/concurrency.py import asyncio _semaphores = { "query_order_status": asyncio.Semaphore(10), "send_work_notification": asyncio.Semaphore(2), } async def invoke_with_semaphore(tool_name, args): sem = _semaphores.get(tool_name) if sem: async with sem: return await asyncio.to_thread(ToolRegistry.invoke, tool_name, args) return await asyncio.to_thread(ToolRegistry.invoke, tool_name, args)这里用了asyncio.to_thread把同步函数丢到线程池,同时信号量控制并发数。外部通知类的工具并发通常要调低,因为消息平台一般有每分钟发送上限。如果工具本身是线程安全的,也可以直接跑在线程池,但共享可变状态时最好还是串行或加锁。
另外还要注意“重试放大”问题。当外部 API 暂时不可用时,如果 Agent 在循环里连续重试三次,并发数会瞬间叠加三倍,把本来就弱的下游打挂。建议重试机制放到工具内部,设置退避策略,例如time.sleep(2 ** attempt),并且单次工具调用最多重试两次。
4.4 实战问题速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 模型总选错工具 | 工具描述里有共同关键词,容易被混淆 | 优化描述,加上具体业务词;或启用预检索 TopK 过滤 |
| 参数报错:TypeError | JSON Schema 类型与函数签名不一致 | 在注册器里做类型强制转换,校验真实函数签名 |
| 返回结果太长,模型忽略 | 没做上下文压缩 | 结果超过 1000 字符就做摘要;完整数据存入可检索存储 |
| 模型幻觉出不存在工具 | prompt 约束不够 | 工具名白名单校验,拒绝无效名称再执行 |
| 二次确认后仍自动执行 | 权限判定放在工具内部或仅在提示词层面 | 必须在调用链路前置代码层判定,且确认状态不可绕过 |
| 并发一高就报错 | 下游限流或共享变量冲突 | 用信号量限制单工具并发,为外部 API 加退避重试 |
| 日志里无法定位哪个会话出问题 | 没有 trace_id | 每次会话生成唯一 ID,打点时间、工具名、参数、耗时、结果 |
| 同一工具反复失败三次后 Agent 还在重试 | 缺少熔断 | 连续 N 次失败后,本次会话内禁止再调用该工具 |
这张表基本覆盖了 Agent-Reach 落地头两周会遇到的大部分问题。实际过程中我最大的感受是:不要过度迷信模型“自知之明”,它能做的只是理解意图,工程上的校验、限流、审计必须由 Agent-Reach 这类框架硬扛。踩坑多次后我形成了一条习惯:每新增一个工具,先写一个“恶意请求用例”测试权限边界,比如让 Agent 尝试删除数据、超范围查询、调用不存在工具,确认都会被拦截,再谈接入正常业务。这样上生产心里才有底。
后面如果想继续扩展 Agent-Reach,我打算给它加上定时任务编排:让 Agent 每天主动触发一次“巡检”,把异常数据通过通知工具发给运维。其实思路和上面的实现一脉相承,只要把触发方式从用户消息改成 cron 调度即可。多动手改一版就知道,把触达范围铺开之后,能做的东西远比想象中多。