这阵子团队内部一直流传一个笑话:我们的智能体(Agent)越建越多,但它们互相之间从来不说话。客服Agent能查订单,库存Agent能查仓库,但客服Agent一旦需要库存数据,只能靠工程师在中间复制JSON、手动转发,链路长还容易错。后来我做了一个叫Agent-Reach的连接层项目,算是把这个笑话终结了。
Agent-Reach 做的事情很纯粹:它给所有智能体和所有内部工具提供一个统一的接入点,大家按照同一套协议完成注册、发现、调用、上报。调用方完全不用关心目标是用 LangChain 写的、Semantic Kernel 写的,还是我们自研的 Agent 框架。只要对方在 Agent-Reach 上登记过能力,我就能以标准方式把它调起来。本文主要面向 AI 应用开发、Agent 平台建设和后端基础设施的同学,如果你正被“Agent 生态碎片化”折磨,这篇内容应该能给你一个可以直接抄作业的落地方案。
1. 为什么需要 Agent-Reach:从孤岛困局说起
1.1 每个Agent都是一座孤岛,这不是夸张
我们公司的现状很有代表性。业务线A用 LangChain 搭了客服助手,数据团队基于 Semantic Kernel 做了报表问答,运维组自己用 Python 写了一个告警解释器。这三套系统内部都接了不少工具,但接口协议、鉴权方式、参数结构完全不同。A 系统定义工具时用的是 OpenAI Function Calling 的 JSON Schema,B 系统用 Semantic Kernel 的 plugin 结构,C 系统干脆就是一堆裸 HTTP 回调。
于是协作就变成噩梦。想让客服Agent调用报表问答能力,工程师得先读懂数据团队那套工具定义的代码,再在客服Agent里手写一个适配器。下一个Agent要调告警解释器时,又得重新来一轮。每个能力都锁在自己的沙盒里,想要打通,只能靠人肉翻译。
点对点连接的问题在Agent少的时候还能忍,Agent一多就是灾难。比如有8个Agent,两两互联最密可以到28条链路。每增加一个Agent,就要拉出一堆新的适配和维护工作。更麻烦的是出问题没法定位:链路一多,没有统一的 trace,A说“我调B超时了”,B说“我这边没收到请求”,C说“我看到你报了错但不是我的问题”,最后只能靠人肉拉群对日志,排查效率极低。
1.2 核心思路:把网状连接改成星型接入
既然点对点连接不可持续,自然就想到了中间汇聚层。我把所有Agent、所有工具、所有需要暴露给Agent的内部能力,全部注册到 Agent-Reach 上。Agent-Reach 就相当于公司内部的“Agent总机”:谁有什么能力,统一登记;谁想调谁,统一接线;每通电话有没有接通、耗时多久、是谁打的,全部留痕。
选星型结构是有取舍的。好处很明显:接入成本从“每个对端一套适配器”变成“只做一次标准接入”;治理从“散落各系统”变成“集中到总线”。坏处也真实存在:多一跳就会多一份网络延迟和单点风险。所以我在设计时把 Agent-Reach 做成无状态网关,路由数据全部放 Redis,节点可以横向扩容,数据包转发做到“能直透就直透”,这些后面会详细展开。
我的结论是:如果你的Agent数量超过5个、跨三个以上团队,星型接入带来的收益会远超那一点延迟成本。如果只有一两个Agent在玩,没必要上这套东西。
2. 核心设计思路与关键组件拆解
2.1 统一消息协议:为什么选择 JSON-RPC 作为底座
设计整个系统的时候,我第一个卡住的地方就是协议选型。市面上可选的无非 REST、gRPC、MQTT、JSON-RPC 这几种。我把它们拉到一起对比过,各有利弊:
| 协议/模型 | 优势 | 在Agent接入中的痛点 |
|---|---|---|
| REST | 普及度高、调试方便、生态成熟 | 只表达“资源增删改查”,表达“调用某个能力”很别扭,请求和会话上下文要靠额外Header塞,语义不统一 |
| gRPC | 性能好、强类型、自带流式 | 接入成本高,跨团队推广需要代码生成和proto管理,浏览器和脚本里调试不方便 |
| MQTT | 适合长连接、异步、广播 | 请求-响应模型弱,但Agent大多数调用还是同步等返回结果,用MQTT反而要额外做关联 |
| JSON-RPC 2.0 | 轻量、天然请求-响应、跨语言、可读性好 | 需要自己约定信封和错误码,依赖上层规范把能力描述补全 |
最后我选了 JSON-RPC 2.0 作为底座,再封装一层 REACH 信封。为什么?因为Agent交互最核心的模式就是“发一个请求,等一个结果”,JSON-RPC 是为此而生的。它不需要像 REST 那样纠结资源语义,跨语言也好处理,任何团队用任何语言都能快速写一个客户端。
REACH 信封长这样:
{ "jsonrpc": "2.0", "method": "agent.reach.call", "params": { "to": "order-agent", "api": "query_logistics", "args": { "order_id": "SO20250101" }, "caller": "customer-service-agent", "trace_id": "tr-8f3a9102e6", "session_id": "s-9c2d11", "auth_ctx": { "user_id": "u-10086", "team_id": "t-sales", "role": "assistant" }, "timeout_ms": 3000, "meta": { "model": "gpt-4o-mini", "source": "web" } } }信封里的to和api是最关键的两个字段,它们决定了这次调用要去找谁。而trace_id、session_id、auth_ctx这些字段是给治理和排查用的,每跳转发都必须透传。你甚至可以把这个信封理解成一张“快递面单”:收件人、商品、订单号、发货人信息都在上面,中转站只看面单就能决定往哪儿送。
2.2 注册中心与能力画像
Agent-Reach 的第二个核心组件是注册中心。任何Agent或工具接入时,都必须先提交一份“能力画像”。这里说的不是简单的接口文档,而是一份机器可读、结构化、可供路由和检索的注册信息。
我用 YAML 来定义这份画像:
endpoint_id: order-agent display_name: 订单助手 transport: type: callback url: https://order-agent.internal/api/callback capabilities: - api: query_logistics description: >- 当用户询问包裹在途信息、物流时间、运输状态时使用。 仅适用于已下单订单,不处理退款物流。 input_schema: type: object properties: order_id: type: string description: 平台订单号 output_schema: type: object properties: status: type: string traces: type: array tags: [order, logistics] idempotent: true timeout_ms: 5000 version: 1.0.0你可能会问:为什么不直接把这些信息放数据库,而要搞一份“画像”出来?因为LLM决定要不要调用某个能力,靠的就是这段description和input_schema。描述写得好不好,直接影响语义路由准不准。这就像景区导览图,图都画不准,游客怎么可能不乱跑。
我的实际经验是:写能力描述时,一定要明确写“什么时候用、什么时候不用”。比如query_logistics描述里加上“仅适用于已下单订单,不处理退款物流”,路由命中率会显著上升。模型拿到描述后,能更准确地判断“用户这句话到底该不该触发这个能力”,而不是靠几个关键词硬猜。注册一份能力画像之后,Agent-Reach 还会自动生成对应的 OpenAPI 描述和兼容 MCP 的工具描述,方便外部系统按标准方式发现能力。
2.3 三层路由:精确、语义、条件
注册中心解决了“能力在哪”的问题,路由引擎解决“请求往哪发”的问题。实际操作中,我把路由拆成了三层:
精确路由:请求里明确带着to=order-agent和api=query_logistics,网关直接去注册表查端点地址,转发完事。这是最常见的模式,毫秒级完成,不引入任何额外计算。
语义路由:调用方只给一句自然语言,比如“帮我查一下这个订单到哪了”,网关先用向量检索召回候选能力,再交给 Rerank 模型做最终选择。这种模式适合自研的“万能前台Agent”,它不需要事先知道目标能力是谁,而是靠语义理解来接。
条件路由:不改变目标和能力,只按参数内容、用户租户、流量比例把请求分配到不同的端点版本,主要用于灰度发布和故障隔离。
| 路由模式 | 路由依据 | 额外时延 | 典型场景 |
|---|---|---|---|
| 精确路由 | 显式指定 endpoint_id + api | 基本为0 | Agent框架内部已经做好工具选择 |
| 语义路由 | 自然语言 + Embedding + Rerank | 数十到数百毫秒 | 万能前台Agent、能力发现 |
| 条件路由 | 参数/租户/流量比例规则 | 基本为0 | 灰度、多租户隔离、版本切换 |
三种路由可以组合使用。我的建议是:大部分调用走精确路由,只有当精确路由失败或者调用方没有指定目标时,才启用语义路由兜底。不要让每个请求都去跑一遍语义检索,那是给性能上刑。在高频链路里,我一般设置语义路由只在候选能力超过3个时才启用Rerank,否则直接取相似度最高的那个。
2.4 安全与治理:从“能通就行”到“可管可控”
做完接入和路由,你会发现一个更现实的问题:安全怎么治理。如果每个Agent用自己的服务账号到处调用,审计日志里根本看不出是哪个真实用户触发的。客户投诉说“这个AI助手帮我重复下单了”,你连是谁在什么上下文里触发的都查不到,这很麻烦。
我在设计中把安全治理拆成几层:
第一层是身份透传。Agent 接入SDK时,会在每次请求的信封里写入真实用户身份信息,比如user_id、team_id、role,网关负责校验签名,确保这些字段不是伪造的。这样下游系统看到的永远是“哪个用户做了这个操作”,而不是“哪个Agent调了我”。
第二层是能力级授权。注意,授权模型不能只放在端点上,必须细化到“端点+api”。比如“库存Agent”这个端点可以被客服Agent调用,但其中“修改库存”这个api只能被仓储系统调用。如果只做端点级授权,等于把一把万能钥匙给了所有Agent,管控等于没有。
第三层是限流与熔断。限流按用户加能力维度做令牌桶,熔断按下游端点做滑动窗口。这一块我在第5章会详细展开。
3. 从0到1部署与接入实操
3.1 第一步:用 Docker Compose 把网关跑起来
讲完设计,我们进入实操。先说部署。为了让团队快速跑起来,我准备了一套最小可用的 Docker Compose 配置,包含网关本体、Redis(注册缓存和限流计数)、Postgres(审计日志存储):
version: "3.8" services: reach-server: image: reach-registry/reach-server:0.12.0 ports: - "8080:8080" environment: REACH_CONFIG: /etc/reach/reach.yaml volumes: - ./reach.yaml:/etc/reach/reach.yaml:ro depends_on: - reach-redis - reach-postgres deploy: replicas: 2 reach-redis: image: redis:7-alpine command: redis-server --appendonly yes volumes: - redis-data:/data reach-postgres: image: postgres:15-alpine environment: POSTGRES_USER: reach POSTGRES_PASSWORD: reach-pass POSTGRES_DB: reach volumes: - pg-data:/var/lib/postgresql/data volumes: redis-data: pg-data:对应的最小配置文件reach.yaml如下:
server: port: 8080 registry: cache: redis://reach-redis:6379/0 ttl: 30s auth: enabled: true jwt_secret: ${JWT_SECRET} audit: enabled: true sink: postgres://reach:reach-pass@reach-postgres:5432/reach route: timeout_ms: 3000 max_depth: 5 rpc_body_limit: 2MB启动命令很简单:
docker compose up -d curl http://localhost:8080/healthz正常情况下,健康检查会返回ok。这里有两个参数我建议特别注意。一个是route.max_depth,它用来控制调用链路的深度,防止Agent之间A调B、B调A形成死循环。另一个是rpc_body_limit,如果某个工具返回特别大的数据包,网关会直接拒绝,免得把内存打爆。
3.2 接入第一个真实工具:写一个天气查询能力
网关跑起来后,我们来接第一个Agent。我用 Python SDK 写一个天气查询服务,做演示再合适不过,因为它足够简单,又能完整展示“提供服务”和“调用服务”两个方向。
先启动一个提供服务端的Agent,它注册在 Agent-Reach 上:
from agent_reach_sdk import AgentServer server = AgentServer( endpoint_id="weather-agent", secret="local-dev-secret", ) @server.register(api="query_weather", idempotent=True, timeout_ms=3000) def query_weather(args: dict): # 这里对接你自己的天气数据源,演示时直接返回固定值 city = args["city"] return { "city": city, "temperature": 26, "humidity": 40, "wind": "三级", } server.run(host="0.0.0.0", port=9201)然后再启动一个调用方Agent:
from agent_reach_sdk import AgentClient client = AgentClient( gateway="http://localhost:8080", token="test-token", ) resp = client.call( to="weather-agent", api="query_weather", args={"city": "深圳"}, timeout_ms=3000, ) print(resp.result)如果不想写代码,也可以直接用 curl 调 Agent-Reach 暴露的 HTTP 网关:
curl -X POST http://localhost:8080/rpc \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "agent.reach.call", "params": { "to": "weather-agent", "api": "query_weather", "args": {"city": "深圳"}, "trace_id": "demo-001", "auth_ctx": {"user_id": "u-test"} } }'返回结果会是一个标准REACH响应。这里有一个操作顺序上的坑:一定要先启动weather-agent,确认它在控制台打印出registered to agent-reach之后,再启动调用方。因为 Agent-Reach 的注册是异步刷新的,如果你同时启动两边,调用方很可能会在注册生效前发起请求,报“能力不存在”。
3.3 完整链路演示:客服Agent调订单Agent再调物流系统
单工具调用很简单,但生产中真正复杂的是链路。我拿一个高频场景举例:客服Agent收到用户消息“帮我查一下订单物流”,客服Agent本身不掌握物流数据,它需要调用订单Agent的query_logistics能力,而订单Agent又要去请求内部物流系统的REST接口。
这条链路在 Agent-Reach 里是这样流转的:
- 客服Agent收到用户消息,LLM决定调用
order-agent.query_logistics,envelope 携带trace_id、session_id、auth_ctx。 - Agent-Reach 精确路由到
order-agent,记录第一跳耗时。 order-agent处理请求后发现需要物流数据,它把请求继续下发给logistics-adapter这个内置连接器。- Agent-Reach 将请求路由到内置连接器,连接器再用 HTTP 方式请求物流系统。
- 结果原路返回,每一层都透传同一个
trace_id。
这条链路在日志里会形成一条完整的追踪记录:
| 时间戳 | span | src | dst | api | 耗时 | 状态 |
|---|---|---|---|---|---|---|
| 10:00:01.001 | span-1 | customer-agent | reach-server | agent.reach.call | 0.5ms | ok |
| 10:00:01.002 | span-2 | reach-server | order-agent | query_logistics | 80ms | ok |
| 10:00:01.010 | span-3 | order-agent | reach-server | agent.reach.call | 0.4ms | ok |
| 10:00:01.012 | span-4 | reach-server | logistics-adapter | query_order_shipping | 120ms | ok |
| 10:00:01.132 | span-5 | logistics-adapter | order-agent | response | 0.3ms | ok |
这类链路配置上有几个经验:第一,内层调用超时一定要比外层短。如果外层给3秒,内层就给2秒,这样失败能快速向上浮出,不会拿着一个已经超时的结果当成功。第二,联调阶段强烈建议开启Mock能力。比如物流系统还没准备好时,直接在 Agent-Reach 里把logistics-adapter的返回写死,客服Agent就可以先行开发,不用等下游系统就绪。
3.4 指标与日志:别等出问题才想起可观测
很多团队把Agent接入网关后就算完事了,等到线上出问题才手忙脚乱。Agent-Reach 这套系统里,我预留了一组关键的表现层指标,上线第一天就接入 Prometheus。
比较关键的指标有这么几个:
agent_reach_calls_total:总调用次数,按 outcome 区分成功失败。agent_reach_call_duration_seconds:调用耗时直方图,用来算 P99。agent_reach_limiter_rejected_total:被限流拒绝的次数。agent_reach_circuit_open_total:熔断器打开的次数。
Grafana 面板里最常用的两条查询语句:
# P99 时延 histogram_quantile(0.99, sum(rate(agent_reach_call_duration_seconds_bucket[5m])) by (le)) # 成功率 sum(rate(agent_reach_calls_total{outcome="success"}[5m])) / sum(rate(agent_reach_calls_total[5m]))排查问题时的标准范式是:拿到业务上报的trace_id,在 Agent-Reach 日志里检索这个 trace,就能看到每一跳的耗时分布,从而定位耗时到底花在“网关排队”“路由决策”还是“下游响应”上。这里提醒一句:网关前面如果还有 Nginx 或 API Gateway,一定要把它们的x-request-id和 Agent-Reach 的trace_id打通,否则排查时要多一步映射,非常痛苦。
4. 踩坑实录与性能调优
4.1 高频问题排查速查表
下面这个表是我在运行过程中整理出来的高频问题,基本涵盖了大部分接入初期会遇到的情况:
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| 调用通但很慢,P99抖动大 | 路由决策里跑了LLM Rerank,或者网关解析了整个大Body | 精确路由不要走LLM;只解析需要路由的字段,其余字节直透 |
| 注册成功但调用报“能力不存在” | 注册中心缓存没刷新,版本不一致 | 检查 Redis TTL,重新注册后强制刷新缓存,确认 endpoint version |
| 多实例部署但回调随机失败 | 实例没上报 instance_id,健康检查异常 | 每个实例必须带独立 instance_id,并确保健康检查探活正常 |
| 下游收到的 auth_ctx 是空的 | 接入SDK没有从当前上下文提取用户身份 | 检查SDK透传开关,确认入口处已解析用户会话并注入身份 |
| 限流不生效,流量照进 | 限流键用了来源IP,但用户共享出口IP | 限流键改用 auth_ctx 里的 user_id + team_id |
| 响应体超过配置大小被截断 | rpc_body_limit设置过小 | 调大 limit,或考虑让工具返回文件引用而不是内联内容 |
这里特别想讲一下限流键选择这个坑。早先我把限流键设在来源IP上,结果发现一个办公室共享同一个出口IP,某个用户疯狂调用时,把整个办公室的流量都限制了。后来改成auth_ctx.user_id + team_id组合键,才真正实现“一人一个配额”,互不干扰。
4.2 性能优化:JSON处理从CPU杀手变成可控项
Agent工具返回的数据结构和传统API很不一样,经常是超长文本、Base64图片、多层嵌套JSON。刚上线那阵,我发现网关CPU莫名飙高,一查才发现所有请求都会被反序列化整个body做校验,某个工具返回了一段几十KB的文本,每个请求都在那里重复解析,白白浪费CPU。
做优化时,我分了三步走:
第一,只解析路由需要的字段。进入网关时,只扫一眼to、api、trace_id,其余字段保持原始字节流不动。这样路由决策还是基于元信息完成的,但不再把工具返回的大对象反复解析。
第二,用更快的数据格式和序列化库。我把标准库的json替换成orjson和msgspec。体感上在长文本场景下,序列化和反序列化的CPU消耗降了一个量级,从堵车变成绿灯。
第三,对“只转发不加工”的调用启用直通模式。这个模式最简单粗暴:请求进来时不做任何解析和重写,直接根据外层信封路由到下游,等响应回来后原样透传回调用方。事实上Agent场景里90%的请求只需要“精确路由+字节透传”,根本不需要网关理解内部业务字段。
4.3 超时重试:别让网关替你盲目重试
超时重试是另一个必须拿出来单独讲的问题。很多团队的常规操作是:下游慢,就在网关层加Retry,结果把“扣款”“发短信”“下单”这种非幂等操作重试了好几次,造成严重的事故。
我的做法是:能力画像里必须有idempotent标签,只有标记为true的能力才允许自动重试。配置示例:
retry: max_attempts: 3 base_delay_ms: 500 max_delay_ms: 2000 jitter_range_ms: 100 only_for_idempotent: true重试的间隔不能是固定值,要用指数退避加抖动:delay = min(base * (2 ** attempt) + random(-jitter, jitter), max_delay)。为什么加抖动?因为如果所有客户端在同一时间失败后等同样长的时间再重试,就会形成“重试风暴”,把已经很脆弱的服务直接打挂。
超时时间的配置也要遵循“层层递减”原则。外层3秒,内层最多2秒,连接器层1秒。这样设计的原因是:一旦某个环节卡住,失败能快速向上层暴露,而不是让整条链路带着一个“假成功”的结果返回给用户。在我实际运行中,把超时做递减之后,P99时延反而下降了,因为很多慢请求不再拖到最外层才超时。
5. 安全、审计与生产治理
5.1 端到端签名:让下游相信“这是网关发来的”
很多Agent系统在做安全设计时,只验证入口这一跳。请求从 Agent-Reach 转发到下游服务时,下游无法确认请求是不是真正来自可信网关。如果内网被攻破,伪造一个请求就能调用所有Agent能力。所以我在网关转发时增加了HMAC签名头,让下游有能力验证请求来源。
签名逻辑不复杂,核心是通过共享密钥计算一个不可伪造的摘要:
timestamp=$(date +%s) payload="POST\n$timestamp\n$body_md5" signature=$(echo -n "$payload" | openssl dgst -sha256 -hmac "$SHARED_SECRET" | awk '{print $2}') curl -X POST "$url" \ -H "X-Reach-Timestamp: $timestamp" \ -H "X-Reach-Signature: $signature" \ -H "X-Reach-Body-MD5: $body_md5" \ --data "$body"下游收到请求后,用同样的密钥重算payload,比对签名是否一致,同时校验timestamp是否在5分钟滑动窗口内,防止重放攻击。这里有两个容易忽视的细节:第一,密钥不要全局共用一个,最好是每个端点一个独立密钥,网关在注册表里取对应密钥签名;第二,密钥不要放进配置文件,用环境变量或K8s Secret挂载,否则代码仓库泄密就等于所有密钥一起暴露。
5.2 审计日志与数据脱敏
Agent 请求经常会带手机号、订单号、住址等敏感信息。如果审计日志把完整请求体都记录下来,合规风险非常大。我之前见过一个项目,审计日志明文存储了用户手机号和家庭住址,后来被安全团队要求全线整改。
Agent-Reach 的默认策略是:审计只记录元信息,包括src、dst、api、trace_id、duration_ms、status。要记录body,必须显式开启完整审计,并且经过脱敏管道处理:
import re # 示意代码,生产环境请使用成熟的脱敏库,比如 presidio、loguru 过滤器 PHONE_PATTERN = re.compile(r"(1[3-9]\d{9})") def mask_sensitive(value: str) -> str: if not isinstance(value, str): return value return PHONE_PATTERN.sub(lambda m: m.group(1)[:3] + "****" + m.group(1)[-4:], value)审计日志自身的存储也要做访问权限控制。数据是密文存储的,查询审计页面需要有审批流程,能看审计日志的人数严格限制。这个不是技术问题,是管理问题,但技术手段能帮上不少忙。
5.3 灰度发布与自动降级
工具端点一旦接入生产,升级就成了随时要面对的事。直接全量切换新版本的风险太大,一旦新版本有Bug,整个AI应用的能力都受影响。Agent-Reach 的条件路由在这里就派上了用场。
灰度配置示例:
routes: - api: query_logistics version: v2 weight: 10 condition: tenant_id: test-tenant先让10%的流量走 v2 版本端点,同时只让灰度租户进来,观察指标平稳后再逐步把weight提到50、100。这条路子跟普通微服务灰度发布一样,但要注意一点:Agent 调用链路上往往涉及多级Agent,你不能只给最外层做灰度,内层依赖的服务也要一起灰度,否则会出现“入口是新版本,出口是旧版本”的错位。
降级策略同样关键。每个能力在注册表里都可以配一个fallback端点,当主端点持续失败触发熔断后,网关自动把流量切到降级服务。降级返回的结果要带有degraded=true标记,这样上层Agent就知道当前是应急状态,可以在话术里告诉用户“暂时查不到,请稍后再试”,而不是生成一个误导性的错误回答。
6. 适用边界与落地建议
最后说点实在的。Agent-Reach 这套设计不是万能的,也不是所有团队都该立刻上。如果你们团队只有一两个Agent、三五个人,点对点直接调就是最快的方式,不需要为了架构而架构。真正适合上这类连接层的场景是:Agent数量超过5个、跨三个以上团队协作、有合规审计压力,或者长期被“接口适配互相推诿”折磨。
落地的时候我建议分阶段走,不要一上来就全量上语义路由。第一阶段只做统一接入和统一日志,把“谁调谁、通没通、多久通”这个问题解决掉,这已经能省下大量排查时间。第二阶段再做身份透传和能力级授权,把安全补上。第三阶段才考虑语义路由和灰度,因为这两个功能依赖前两阶段沉淀下来的稳定基础。
我个人做完这个项目的最大体会是:中间层不是越复杂越好。Agent基础设施跟其他中间件一样,最核心的价值是让“下一次接入变简单”。你每加一个Agent,接入成本不是线性增长而是接近常数,这套系统才算真正成功。
最后再分享一个小技巧:如果你后面要上语义路由,记得把每个能力的description当成对外接口文档来写,不要只写“查询物流”四个字,而是要写清楚“什么时候用、什么时候不用、参数怎么填”。甚至可以拿大模型来帮你改写这些描述,让它变得语义更完整。我实测下来,这一步对路由准确率的提升比换更强的Embedding模型还明显。