AgentScope第10式·通玄MCP

AgentScope第10式·MCP服务

知识库不该住在 Agent 进程里,MCP 是边界不是外挂。

[读者] 正把数字人从 Demo 推进企业内部试点的 Java 工程师
[痛点] 检索代码和向量库客户端都塞在 Agent 进程里,换数据源就要改 Agent
[现在读] 第9式的分支已经冻结,知识库要以服务形态接进来
[读完] 能独立设计 MCP 接入、工具命名空间、注册生命周期与失败语义
[旧方案] 把检索逻辑写进 Agent 进程内的 Tool
    |
    v
[新需求] 知识库独立部署、独立鉴权、独立扩缩容
    |
    v
[冲突] Agent 与知识库的发布节奏、生命周期完全不同
    |
    v
[后果] 知识库换个分片策略,Agent 就得重新打包发版

我是老李。这个系列写到第 10 式,数字人 Demo 已经不在“能不能跑通”的阶段,而在“能不能进企业内网试点”的阶段。前九式我们一直在往 Agent 内核里加东西:工具、状态、记忆、计划。加到这一式,我第一次主动决定——往回收一点。

为什么要收?因为企业里“知识”从来不是一段 systemPrompt 能装下的东西。它是一套由知识库团队独立维护的服务,有自己的发布窗口、鉴权体系、扩缩容策略,甚至有独立的 SLA。

上一式结束时,我们冻结了 chapter/09 的分支状态;这一式从它继续切出 chapter/10-mcp-knowledge,只做一件事:让数字人能基于证据回答企业知识问题,并且证据可追溯、失败可解释。

  • 知识库既然是“数据”,为什么不能像读数据库一样在 Agent 里直接查?

  • MCP 的三种接入方式该怎么选,选错了会付出什么代价?

  • 如果 MCP 服务挂了,数字人应该“说不知道”,还是“编一个”?

01、故事

现场是一次内部评审。演示环境里,数字人回答“年假怎么算”答得挺顺,评审席上知识库团队的负责人问了一句:这套检索逻辑,是你们自己写在我们的库上,还是我们提供接口?

我们当时是前者。第 4 式把 Tool 加进来时,为了让 Demo 尽快跑通,我们直接在一个 Tool 里连了知识库的检索接口:向量库地址、鉴权 token、分片规则,全在 Agent 进程里。

这一式的任务是:把知识检索从 Agent 进程里搬出去,变成一个独立部署的知识服务,再通过 MCP 接回来。

触发变化的是三件小事。知识库团队下个月要换 embedding 模型;运维要求数字人和知识库分开扩容;安全组不同意把知识库的 token 放进 Agent 容器。

冲突就很明显了:Agent 一周发一次,知识库一季度动一次,两者绑在一起,谁也别想动。

知识住在别处
边界先于能力
进程各自生长
契约才是接口

02、问题

旧认知:知识库检索就是“一个 Tool”,写好函数体就行。

为什么不够:Tool 是进程内的抽象。它默认调用方和被调用方共享生命周期、共享类加载器、共享故障域。而企业知识库,这三条一条都不满足。你写的是 searchKnowledge,实际上你写的是“把知识库的发布、鉴权、超时、限流全部搬进 Agent 进程”。

还有一条更容易踩的坑:某些 AgentScope 历史版本提供过 Knowledge/RAG 之类的内置抽象。看到“框架自带知识库”就顺手拿来当新项目主线,是本篇最典型的误判。Java v2.0.3 的构建块主线是 Agent、Tool、状态与记忆这类通用能力,知识检索属于领域能力;旧 Knowledge API 是否保留、形态如何,需要按当前官方文档核验,不要拿旧版本的类名去推新版本的行为。

技术表现上,旧做法会同时暴露三个症状:Agent 镜像里出现向量库 SDK;一次知识库网络抖动就会让整轮对话超时;回答里的引用无法追溯,用户没法验证。

本篇的完成标准只有三条:

  1. 问题命中知识服务后,回答中携带文档 ID 与证据摘要;

  2. MCP 服务不可用时,有明确失败语义,不出现“无证据自信回答”;

  3. 知识服务的部署、鉴权与 Agent 的发布解耦。

耦合不在代码
耦合在发布节奏
数据本可独立
内核不该越界

03、原理

先把 MCP 的边界用一句话钉住:MCP 是工具与资源在进程之间的契约,它规定的是“怎么描述、怎么发现、怎么调用”,不规定模型有多聪明。它不提升推理能力,只改变工具的归属。

再看三种传输方式。STDIO:宿主进程拉起一个 MCP server 子进程,用 stdin/stdout 传 JSON-RPC。它没有网络层,隔离简单,适合本地开发、CLI 工具、单机插件,但它没法独立扩缩容,也没法让另一个团队部署。SSE:HTTP + SSE,一个 POST 通道上行、一个长连接下行,是 MCP 早期规范(2024-11-05)引入的传输方式,后来被 Streamable HTTP 取代。Streamable HTTP:MCP 2025-03-26 规范引入,单端点 POST/GET,可选升级为 SSE 流,天然适配反向代理、网关、鉴权中间件,是企业内网服务端接入的正解。

反直觉判断:MCP 带来的最大变化不是“多了几个工具”,而是把工具的所有权从 Agent 进程转了出去——失败模式也随之转了出去。以前“这个工具不会挂”,现在“这个工具会挂,而且会超时、会鉴权失败、会被限流、会返回 429”。谁接入 MCP 却不处理这四种失败,谁就是在给幻觉开后门。

工具命名空间是第二个容易忽视的点。本地 Tool 加多个 MCP server 共存时,工具名是全局视图。search 这种名字一旦重复,行为就取决于注册顺序,而不是取决于你的意图。约定是前缀化:kb.searchKnowledge、crm.getCustomer。前缀不是装饰,是故障隔离和可读性。

注册生命周期要显式对待。一次完整的注册包含:建立连接 → initialize 握手 → tools/list 拉取工具清单 → 按白名单过滤 → 注册进 Toolkit → 运行时调用 → 断线重连或注销。关键判断是:注册发生在启动阶段,不是每次用户提问时;而且不能假设它永远成功,启动期注册失败必须有降级路径。

工具过滤同样不是可选项。企业知识服务可能暴露几十个工具(检索、分页、写回、重建索引),全量注册给模型会直接吃掉上下文预算,还会显著提高误选概率。白名单只放 searchKnowledge 与 getDocument 两个,其余工具留给运维脚本,不交给模型。

工具换了主人
失败随之外移
命名需带前缀
生命周期显式

04、架构

[输入] 用户语音/文本问题
    |
    v
[模块] Realtime Bridge → AgentScope Agent(ReAct + Toolkit)
    |
    v
[数据/状态] 工具注册表:本地兜底工具 + MCP 工具(带 kb 命名空间)
    |
    v
[处理] 模型决策 → MCP Client → 知识服务 MCP Server → 检索与取文档
    |
    v
[输出] 带证据的回答(docId + 摘要)或明确失败语义

边界:知识服务的职责是检索、排序、取文档、返回结构化证据;Agent 的职责是决策是否检索、如何组织答案、如何标注证据。MCP 只承载这两者之间的契约,不承载业务规则。

收益:知识库可以独立部署、独立扩容、独立鉴权;Agent 不再打包向量库 SDK;知识库换 embedding 模型不影响 Agent 发版。

代价:多一次网络跳转,多一个部署单元,多一类必须处理的失败。工具调用的 P99 不再由你一个人决定。

适用条件:知识需独立授权、有独立维护团队、调用量足以摊平运维成本、回答需要可追溯。

不适用条件:单机 Demo、一次性脚本、知识量小到能进 systemPrompt、工具调用频率极低且无授权要求——这时候引 MCP 是给自己加部署负担。

涉及 AgentScope 内部机制时,工具注册与管理的入口是 Toolkit,固定版本路径为 agentscope-core/src/main/java/io/agentscope/core/tool/Toolkit.java(v2.0.3 源码),配套文档是 官方 tool.md。涉及 MCP 传输与工具清单的具体实现细节,以该 tag 下 agentscope-core 的 tool 相关包为准。

一图分四层
进程各自安
契约定边界
失败要有声

05、实战一次

环境与依赖基线统一固定在 v2.0.3(releases/tag/v2.0.3),JDK 与 Maven 版本以该版本官方文档为准。分支:chapter/10-mcp-knowledge,从第9式分支继续创建。

第一步,知识服务独立成模块。它是普通 Spring Boot 应用,对外同时提供 REST(给客服、IM 等非 Agent 调用方)与 MCP 端点(给 Agent)。核心的结构化返回是证据,而不是一段文本。

public record KnowledgeHit(String docId, String title, String snippet, double score) {
}

@RestController
@RequestMapping("/knowledge")
public class KnowledgeController {

    private final KnowledgeSearchService searchService;

    public KnowledgeController(KnowledgeSearchService searchService) {
        this.searchService = searchService;
    }

    @PostMapping("/search")
    public SearchResponse search(@RequestBody SearchRequest request) {
        List<KnowledgeHit> hits = searchService.search(request.query(), request.topK());
        return new SearchResponse(hits, hits.isEmpty() ? "NO_HIT" : "OK");
    }

    @PostMapping("/document/{docId}")
    public DocumentResponse document(@PathVariable String docId) {
        return searchService.getDocument(docId);
    }
}

第二步,Agent 侧装配。教程仓库的配置段是我们自己的配置结构,由我们自己的装配类读取,不是框架内置配置键:

knowledge:
  mcp:
    transport: streamable-http
    url: http://knowledge-svc:8080/mcp
    namespace: kb
    allow:
      - searchKnowledge
      - getDocument
    timeout: 3s

第三步,注册进 Toolkit。下面这段是预期装配结构,未在当前环境实测;其中 MCP 客户端构建入口与远程工具注册方法签名,请在 v2.0.3 的 io/agentscope/core/tool/ 目录与官方 tool.md 中核对,本系列不根据类名猜行为。

@Configuration
public class ToolkitConfig {

    @Bean
    public Toolkit toolkit(KnowledgeMcpProperties properties) {
        Toolkit toolkit = new Toolkit();

        // 本地兜底工具:知识服务不可用时,明确回“我查不到”
        toolkit.registerTool(new NoEvidenceAnswerTool());

        // 远程知识工具:命名空间 kb,白名单只放两个工具
        toolkit.registerRemoteTools(
                "kb",
                McpEndpoint.streamableHttp(properties.url()).timeout(properties.timeout()),
                Set.of("searchKnowledge", "getDocument"));

        return toolkit;
    }
}

第四步,启动与请求。先用 REST 验证知识服务本身可用:

curl -s -X POST http://localhost:8080/knowledge/search \
  -H 'Content-Type: application/json' \
  -d '{"query":"年假怎么算","topK":3}'

示例输出(未在当前环境实测,以下为预期结果):

{
  "code": "OK",
  "hits": [
    {"docId": "HR-2024-017", "title": "员工休假管理办法", "snippet": "入职满一年可享年假 5 天……", "score": 0.83}
  ]
}

第五步,验证数字人。打开 Web 运行页 /run/{projectId},问“年假怎么算”,期望回答里出现 HR-2024-017 和对应摘要;然后把知识服务停掉再问一次,期望得到明确的“知识服务当前不可用,无法给出有依据的回答”,而不是一个编出来的天数。这两个动作,就是本篇的验收动作。

先跑最小链路
再谈高级能力
证据必须落地
结果不可假设

06、排查

证据链一:服务停了,数字人还在自信回答。

现象:知识服务容器被停掉后,数字人依然能给出“年假 5 天”这类回答,且不带任何引用。怀疑是模型在编。检查方式是从 Bridge 侧打开工具调用日志,看到 kb.searchKnowledge 确实被发起,但返回被当成空列表处理。证据是调用链上 catch 之后返回了空集合而不是错误。根因不在模型,在我们的工具实现:把工具异常吞成了“没查到”,而“没查到”和“查不到”在模型眼里是两回事。修复方向是让失败具备类型和语义。

错误尝试: 第一反应是在 systemPrompt 里加一句“必须调用知识库工具,不允许凭记忆回答”。这个做法错在归因方向——模型本来就调用了工具,问题出在工具返回的语义上。加提示词只能提高调用率,不能修复“异常被吞成空结果”这个事实。更糟的是,它会掩盖真实的失败率。

证据链二:工具名冲突与端点选错。

现象:本地已经有一个 search 工具,接入知识服务后,模型有时调用本地那个、有时调用远程那个,行为不稳定。同时 MCP 端点从 SSE 切到 Streamable HTTP 时,网关返回 405。判断一:以为模型不稳定,实际是工具名重复,注册顺序决定了谁被覆盖。判断二:以为 URL 写错,实际是网关只放行 GET 而 Streamable HTTP 的主通道是 POST。

修正做法:命名空间前缀化 kb.search;网关按 Streamable HTTP 放行 POST/GET,并允许 text/event-stream 响应类型。

上游贡献判断:本章两条证据链的根因都在我们自己的装配与工具实现上,可在最小 Demo(chapter/10-mcp-knowledge)中复现,与框架版本无关。v2.0.3 与 main 是否在“工具失败返回语义”上有更明确的文档描述,需要现场核对官方文档与源码;在核对之前,只能记录为候选。

现象先于怀疑
证据先于结论
静默最是危险
错误要说出口

07、优化

基于第 06 章的证据,V2 做四件事。

第一,命名空间强制。所有远程工具注册名一律带 kb. 前缀,本地工具用 local. 前缀,从源头消除“谁覆盖了谁”的不确定性。新行为是工具清单稳定可枚举。

第二,白名单过滤。只注册 kb.searchKnowledge 与 kb.getDocument。理由是上下文预算和误选率。验证方式是启动日志打印注册后的工具清单,数量应恰好为预期值。

第三,失败语义结构化。工具返回从“一段文本”改为“结果 + 状态码”,至少区分 OK、NO_HIT、TIMEOUT、UNAVAILABLE、UNAUTHORIZED 五种。只有 OK 允许生成带结论的回答;NO_HIT 走“知识库中没有相关记录”;其余三种走“知识服务当前不可用”兜底工具。

第四,证据回填。OK 时把 docId 与 snippet 一并交给模型,并在 systemPrompt 里明确要求答案中标注文档 ID。这一步是“可追溯”的唯一来源。

为什么这样改:把“模型该怎么答”从提示词技巧,转换为工具返回值约束。新行为是同一句提问,知识服务可用与不可用时给出两类完全不同的回答,且都可解释。验证方法见第 05 章的两个验收动作。

命名加前缀
过滤留白名单
失败有语义
回答带证据

08、演进

[同一输入] 用户问“年假怎么算”
    |
    +--[V1] 检索代码在 Agent 进程内 / 异常被吞成空 / 回答无可追溯证据
    |
    +--[V2] 知识服务独立部署,MCP 契约 / 失败带语义 / 回答带 docId
    |
[Trade-off]
得到:解耦、可独立扩缩容、可追溯、可复用到非 Agent 渠道
失去:一次网络跳转、一个额外部署单元、必须处理超时与限流

正确性:V1 在知识库变更时会静默降级,V2 用状态码把降级显式化。

稳定性:V1 的故障域是 Agent 进程,知识库抖动会拖垮对话;V2 把故障圈在知识服务内,Agent 有兜底路径。

复杂度:V1 一个类,V2 一个服务加一份契约。这是本式唯一真实付出的成本。

成本:多一个进程、多一套部署与监控、多一条网络链路。

适用范围:V2 在企业内网、多调用方、需授权与审计的场景显著占优;单机 Demo 不值得。

遗留问题:MCP 连接的重连策略、工具清单变更时的热更新,本式尚未处理;评测体系也没有建立,无法回答“接上知识库后准确率提升了多少”——这个数字在没有评测集之前不能编。

旧路仍在走通
新路更贵一层
取舍不在当下
在三年维护里

09、洞见

9.1 反直觉判断:MCP 不是“更强的 Tool”,而是“把工具变成外部依赖”

多数人把 MCP 理解成“跨语言调工具”,于是关注点全在协议上。真正的变化在依赖方向上:进程内调用是可信的,跨进程调用是不可信的。工具接入方式一变,“工具不会失败”这个隐含假设立刻失效。所以第 07 章优先做的是失败语义,而不是接更多的工具。

9.2 反直觉判断:知识库越“智能”,越应该把它当普通服务

RAG 常被当成 Agent 的“内置能力”,于是向量库、分片、重排全被塞进 Agent 内核。方向是反的:检索环节越复杂,越需要一个能被独立观测、独立压测、独立替换的部署单元。把它当普通 HTTP 服务,反而更容易演进。

9.3 什么时候不应该用本章能力

四种情况别引 MCP:单机 Demo 与一次性脚本;知识总量小到能直接进 systemPrompt;工具调用频率极低且无授权、无审计要求;团队没有能力维护第二个部署单元。这四种情况下,一个进程内的 Tool 更快、更省、更好排查。MCP 的价值来自解耦,无耦合可解时它就是纯成本。

9.4 失败语义比召回率先行

在评测集建立之前,谁也不知道召回率是多少,但任何人都能立刻定义“查不到时该说什么”。可观测的失败语义是低成本、高收益的一步,它把不可验证的“模型乱答”问题,转换成可断言的工程问题。

反直觉处藏真
边界比能力贵
不为用而用
何时不用更贵

10、系统落地

原来有什么:一个能在 Demo 环境跑通知识问答的数字人,检索逻辑与向量库客户端都在 Agent 进程内,回答无法追溯。

本篇新增什么:独立部署的知识服务;MCP(Streamable HTTP)契约;kb. 命名空间与工具白名单;五态失败语义;回答中的文档 ID 与证据摘要;分支 chapter/10-mcp-knowledge,建议提交 feat(ch10): integrate MCP knowledge service。

现在能做什么:数字人能基于证据回答企业知识问题,并在知识服务不可用时明确说“查不到”,而不是编一个答案。

还缺什么:没有评测集,无法量化回答质量;没有对 MCP 连接重连与工具清单热更新的处理;没有把知识检索的延迟纳入可观测面板。

下一步如何演进:先补评测与可观测,让“接上知识库到底变好还是变差”变成可测事实,再考虑多知识源路由与工具权限分级。

开源参与点:发现可作为 Issue/PR 候选的问题——“工具调用失败时的错误语义在官方文档中的说明粒度”是本章实际遇到的疑点,但它是否属于框架缺口,需要先在 v2.0.3 与 main 之间做对照,并搜索官方 Issues 与 Discussions 后再决定是否提 Issue;在此之前不声称已提交、也不假设上游存在问题。

原来能对话
现在能举证
缺的是评测
下一步是稳

11、小结

Q1 → 知识是数据,但检索是服务;服务有自己的生命周期,不能寄居在 Agent 进程里
Q2 → 本地开发用 STDIO,服务化部署用 Streamable HTTP,SSE 属于上一代传输
Q3 → 服务不可用时必须明确说不知道;有证据才允许给结论
状态 → chapter/10-mcp-knowledge 可运行:回答携带 docId 与证据摘要,服务不可用有明确失败语义

一问知来源
二问定边界
三问明失败
一式收一心

12、作业

12.1 理解题:为什么工具命名空间前缀在企业场景里不是可选项?

参考答案:工具清单是全局视图,多个 MCP server 与本地 Tool 共存时,同名工具的行为由注册顺序决定,而不是由你的意图决定。前缀把“冲突”变成“显式区分”,同时让日志、监控、权限策略可以按来源归类。

12.2 实战题:在 chapter/10-mcp-knowledge 上,把知识服务停掉,验证数字人不会给出无证据结论。

参考答案:先确认 kb.searchKnowledge 返回 UNAVAILABLE 或 TIMEOUT,再确认兜底工具被触发,最终回答中不含任何具体政策结论。若仍出现结论,说明工具异常仍被吞成 NO_HIT,需要回到第 07 章的状态码改造。

12.3 排障题:MCP 端点从 SSE 切到 Streamable HTTP 后网关返回 405,如何定位?

参考答案:405 表示方法不允许,优先检查网关是否只放行 GET。Streamable HTTP 的主通道是 POST,需要在网关放行 POST/GET,并允许 text/event-stream 响应类型;同时确认反向代理没有缓冲流式响应。不要先怀疑框架,先看网关。

12.4 架构判断题:打开 v2.0.3 的 Toolkit.java 与官方 tool.md,判断“工具注册”发生在 Agent 生命周期的哪个阶段,以及注册失败时应当如何降级。

参考答案:本题要求读者自行打开固定版本源码与文档核对注册入口与时机,不能只背本文结论。判断要点是:注册属于启动期行为而非请求期行为,因此注册失败必须在启动阶段暴露,并走本地兜底工具,而不是等到用户提问时才发现工具不存在。具体方法签名与工具清单拉取时机,以 v2.0.3 源码与该 tag 下的官方文档为准;若文档与代码不一致,以源码为准并记录核验依据。

题在书上浅
功在源码深
不看调用链
只背结论空

13、思考

这一式的核心冲突只有一个:知识该住在哪里。住在 Agent 进程里,你得到的是短期便利,付出的是长期发版耦合;住在独立服务里,你付出一次网络跳转,得到的是可独立演进、可追溯、可复用的知识能力。

判断标准可以复用到任何“内置还是外置”的选择上:当被集成方的生命周期、鉴权体系、扩缩容策略与调用方不一致时,就外置;一致时,内置更省。MCP 只是这条判断在工具领域的一种落地形式,它本身不产生价值,解耦才产生价值。

而“会用框架”和“能参与框架演进”的差别,恰恰在于你是否愿意打开固定版本的源码和文档,去核对那一个方法签名、那一个注册时机,而不是满足于让 Demo 跑起来。

会用是本事
会改是参与
一念分内外
一证定真假

posted @ 2026-10-11 11:16  李福春  阅读(3)  评论(0)    收藏  举报