Skip to content

feat: tools, knowledge retrieval, MCP and sub-agents - #4

Merged
sinabs merged 20 commits into
mainfrom
feat/03-tools-rag-mcp
Nov 13, 2026
Merged

sinabs merged 20 commits into
mainfrom
feat/03-tools-rag-mcp

Conversation

@sinabs

@sinabs sinabs commented Nov 13, 2026

Copy link
Copy Markdown
Owner

概述

第 03 轮:给 Agent 装上工具和知识库,并接入外部 MCP 服务、开放子 Agent。每多开放一种能力,权限边界就多一块,所以这一轮的重点是在不放松边界的前提下把能力装上去。

主要改动

工具

  • calculate:手写递归下降解析器,不用脚本引擎,限制表达式长度、嵌套深度、数字位数,除零明确报错。
  • get_current_time、list_my_notes:业务上下文(UserContext)走 RuntimeContext 按类型注入,不进工具 Schema,模型看不到也伪造不了,取不到就拒绝。

知识库检索(retrieve)

  • 检索放在应用层的 RetrievePort 后面,knowledge 包不依赖 io.agentscope(ArchUnit 守着);做成一个工具、只走一条通道,不挂框架的自动注入。
  • 三层:关键词(BM25,永远可用,兜底)、向量(最低相似度过滤)、RRF 融合;两路并发,任何一路失败退回另一路,全部失败才报错。
  • 向量化客户端有两个:OpenAI 兼容的 /embeddings,和火山方舟多模态的 /embeddings/multimodal(一条文本一次请求),由 yoke.knowledge.embedding.protocol 选择;local 环境接方舟,复用 ARK_API_KEY。应用就绪后在后台预建向量索引。
  • 引用编号用片段自己的稳定标识(文档名加正文哈希前 8 位),不再每次检索都从 [1] 数起,避免模型多次检索后引用对不上;重名文档加载时直接失败,围栏代码块里的 # 不再被当成标题。

MCP

  • 只允许 HTTP 传输(sse、streamable-http),stdio 在配置绑定阶段就拒绝,连不上启动即失败;连接按相反顺序释放,失败不掩盖主异常,报错里不带地址中的令牌。
  • 只注册服务端声明了 readOnlyHint 的工具(HTTP 接口没有人工确认通道,其余工具会让一轮对话无声结束);工具名与已有工具或 Harness 内置工具重名时启动失败。

子 Agent

  • 代码里声明 summarizer,工作区文件声明 kb-researcher;子 Agent 不继承 disableWebTools,联网禁用靠 ToolsConfig.deny 兜底;代码与工作区文件同 id 冲突(框架静默以文件为准)改为启动即失败,扫描以工作区目录里实际的文件为准。

包结构:检索实现在 knowledge.retrieve,向量化客户端在 knowledge.embed,Passage、UserContext 在 model,SharedToolkit 在 tool。

测试

  • ./gradlew clean build:332 个测试,0 失败,0 跳过。
  • 精确集合测试守着主 Agent 和各个子 Agent 实际能看到的工具清单;MCP 部分用进程内的真实 MCP 服务端(两种传输都真实握手、列工具、调用)。
  • 用真实模型通过 POST /api/chat/stream 把各个工具各跑过一遍,方舟向量模型也对真实接口验证过。

已知限制

  • 配置了的 MCP 服务连不上会导致应用启动失败,没有「可选服务」和后台重连。
  • 流式接口的请求体里没有用户标识,所有调用方归属 default 用户,共用同一份笔记和长期记忆。
  • 第三方 MCP 服务(TLS、鉴权失败、断线重连)和 OpenAI 兼容向量服务只用本地假服务测过。

sinabs added 20 commits November 1, 2026 23:23
给 Agent 装上工具、知识库、MCP 和子 Agent,并同步收紧权限。

- 工具:ChatTools 提供时间、四则运算(手写递归下降解析,限制长度、
  深度和数字位数)和按调用者隔离的示例工具;调用者身份通过
  RuntimeContext 按类型注入,不进入工具的参数 Schema
- 检索:retrieve 工具返回带 [1][2] 编号的引用;knowledge 包自带
  RetrievePort,关键词检索兜底,配置了 embedding 服务时与向量检索按
  RRF 融合,任一路失败退回另一路。不采用已弃用的 rag-simple
- MCP:只允许 HTTP 传输,配置 stdio 时启动即失败;只注册声明为只读
  的工具,因为 HTTP 接口没有人工确认通道
- 子 Agent:编程式声明 summarizer,工作区 Markdown 声明
  kb-researcher;与代码或内置子 Agent 重名时启动失败(2.0.4 默认是
  文件覆盖);子 Agent 通过 ToolsConfig 拿不到联网、文件和 shell 工具
- 权限:显式传入 toolsConfig,避免读取工作区 tools.json 拉起 stdio
  进程;disableWebTools 与 deny 并用,工具清单由测试精确固化
- 补充工具、检索、MCP、子 Agent、权限和端到端对话测试,README 中英同步
解决冲突:
- 保留 03 的 AgentConfiguration 装配 HarnessAgentFactory(共享 Toolkit 与子 Agent),放弃 02 里把工厂改成 @component 的写法
- 02 的读类记忆/会话工具黑名单并入 YokeSubagents.DENIED_TOOLS,对主 Agent 和所有子 Agent 生效
- 天气工具注册进共享 Toolkit,工具清单相关测试同步加上 get_weather
- 03 的工具与端到端测试改用 AG-UI 流式事件,指定 userId 的用例改走 /api/chat
- README 中英与 ChatApi 文档按合并后的工具清单更新
- 知识库文档同步 AG-UI 流式协议、get_weather 和被禁用的工具
- MCP 启动失败不再把原异常作为 cause,避免地址里的令牌进日志
- listTools 加超时;MCP 工具与已有工具重名时启动失败
- 判空写法统一为 StrUtil、Objects、Optional
- 混合检索改为 flatMapSequential:向量与关键词两路同时发起,向量服务慢或挂了不再拖住关键词兜底
- 向量检索加最低相似度(默认 0.3),问无关问题时返回空,而不是凑够 K 条无关资料
…owledge base

- retrieve 的工具说明加一句:用户明确要求查知识库时必须调用,不能没查就说没有
- 真实模型跑出来的现象:只问"请查一下知识库"时模型一次都没调 retrieve,直接说知识库是空的
- 加测试守着这句说明
- 代码注释、测试说明和一条启动报错信息里的「2.0.4」统一写成「AgentScope」
- 版本号只留在交代技术栈的地方,升级框架时不用满仓库改注释
…ledge packages

新增火山方舟多模态向量客户端,并整理知识库相关包结构。

- 新增 ArkEmbeddingClient:方舟的向量模型走 /embeddings/multimodal,
  请求体是内容对象数组、不支持批量、响应的 data 是单个对象,和 OpenAI 兼容的
  /embeddings 完全不同,只改 base-url 会被方舟直接拒绝。一条文本一次请求,
  并发上限 4,结果按输入顺序,并校验返回维度。
- 新增配置 yoke.knowledge.embedding.protocol(openai | ark-multimodal,
  默认 openai);local 环境改为 ark-multimodal,模型 doubao-embedding-vision-251215,
  密钥复用 ARK_API_KEY。
- 包结构:检索实现移到 knowledge.retrieve,向量化客户端移到 knowledge.embed,
  Passage 和 UserContext 移到 model,SharedToolkit 移到 tool(model 包不允许依赖框架)。
- 修复 KnowledgeCorpus 重构引入的回归:片段正文在 chunk 还是空时就拼好,
  导致所有片段只剩标题。
- SubagentTests 的 NEVER 补上 5 个读类记忆和会话工具,子 Agent 读不到
  别的用户的数据也有测试守着。
- 测试随包搬迁;新增 ArkEmbeddingClientTests 和装配层用例,假服务只注册
  多模态路径,误请求 /embeddings 会变红。README 中英文同步。

已用真实方舟验证:标准 /embeddings 会拒绝该模型,多模态接口返回 2048 维向量,
向量检索对改写后的问题仍能命中同一篇文档,对无关问题返回空。
retrieve 返回的每条资料改用片段自身的标识(形如 [api-usage.md#2])做引用编号,
不再每次检索都从 [1] 开始编号。同一轮对话里模型换个说法再查一次时,两次结果里的 [1]
会指向不同片段,引用就对不上了。工具说明、kb-researcher 提示词、知识文档、ChatApi
和两份 README 同步更新。
片段 id 原来是「文档名#片段序号」,现在它是写进 Redis 对话历史的引用编号,跨重启、
跨部署都还在。文档前面插一节再部署,后面所有片段的 id 就换成了别的正文,旧引用会指错。
改成「文档名#正文 SHA-256 前 8 位」:同一个 id 一定是同一段正文,正文改了 id 跟着变,
旧引用只会找不到,不会指到别处。补了插入小节、修改正文两个测试。
片段 id 以文件名开头,而 classpath*:knowledge/*.md 会把每个 classpath 根(依赖 jar、
测试资源)里的同名文件都找出来。两篇同名文档的片段会共用 id 前缀,融合检索按 id 去重时
一篇会悄悄覆盖另一篇,引用也分不清出自哪篇。现在加载时发现文件名重复就直接失败并列出
重复的文件名。加载逻辑拆出 load(List<Resource>) 以便测试。
retrieve 工具说明、kb-researcher 提示词和知识文档里举例用的 [api-usage.md#2] 原本
正好是一个真实片段的编号。模型被要求原样照抄方括号里的编号,可能把这个没检索到的片段
也当作引用写出来。示例统一改成 [文档名.md#片段编号](README 英文版用
[doc-name.md#passage-id]),并加测试确认这些给模型看的文本里不出现任何真实片段编号。
改成片段 id 引用时,断言从 contains("[2] 来源:").doesNotContain("[3]") 换成了只匹配
一条编号的正则,不再检查条数:RetrieveTool 不把 top_k 传给检索端口时测试照样通过。
现在数一遍结果里带编号的资料,要求正好两条(脚本里模型要的 top_k=2)。
「多次检索的编号不冲突」原来只靠 doesNotContain("[1]") 之类的断言,不同片段被格式化成
同一个编号时测试照样能过。现在把两次结果里的编号按顺序取出来,精确比较,并要求四条资料
里只有共享的那条编号重复。
MCP 工具的重名检查原来只和启动时的 Toolkit 模板比对,而 memory_save、agent_spawn
这些 Harness 内置工具是每次 build() 时才加进副本的,模板里看不到,外部服务公布同名
工具时检查拦不住。现在维护一份 HARNESS_TOOL_NAMES 一起比对,并在 HarnessAgentToolsTests
里用真实 Agent 核对名单没有漏项。listTools() 返回空时也改为报明确的错误,而不是空指针。
向量索引原来在第一个用户调用 retrieve 时才建。方舟多模态接口一条文本一次请求(并发 4、
单次读超时 15 秒),第一个用户的这次工具调用要等所有片段向量化完。现在 RetrievePort 加了
默认空实现的 warmUp(),应用就绪(ApplicationReadyEvent)后由 VectorRetrievePort 在
boundedElastic 上预建;预建在途时来的检索等同一个索引,不会重复建;预建失败只记日志,
启动照常,下一次检索重建。挂在就绪事件上,切片测试和未起完的上下文不会对外发请求。
KeywordRetrievePort 每次查询都把每个片段的词频表重新累加一遍求长度,而长度在构造时就
已经算过(tokens.size())又扔掉了。现在存成 int[] lengths 与词频表一一对应。补了一个
长度归一化的测试,长度与片段对错位时会变红。
按项目的判空约定,「为空就用默认值」的三元表达式改写成 Optional:
Objects.isNull(response) ? null : response.get("data") 改为
Optional.ofNullable(response).map(...).orElse(null)。行为不变。
知识文档切片时,以「# 」开头的行都被当成文档标题并从正文里删掉,代码块里的 shell 注释
(例如 # 先切 JDK)会把之后所有片段的标题改掉。现在识别 ``` / ~~~ 围栏,围栏内的行
原样进正文,不参与标题判断。
用 IDEA 检查本分支改动过的 53 个 Java 文件,修掉能修的 warning,行为不变:
- 主代码:toString 补 @nonnull;YokeSubagents 删掉未使用的 import,取文件名为空时
  报出是哪个资源;ChatTools 先算出实际使用的时区名再解析;KnowledgeCorpus 抽出重复的
  片段拼接。
- 测试:未用到的 lambda 参数改成 _;block() 改成 blockOptional().orElseThrow();
  轮询加 Thread.sleep 改成 CountDownLatch;MCP 测试服务换掉已弃用的
  addServletMappingDecoded 和 CallToolResult 构造器;预期抛异常的 jedis()/connect()
  后面补 close();能用方法引用的改成方法引用;冗余的正则转义、空 try 块、只传固定值的
  形参一并清理。
剩下的是误报:ArchUnit 的 @archtest 字段、反射调用的工具方法、环境变量占位符、校验
http:// 前缀的字符串字面量。
版本号从 2.0.0 升到 3.0.0,对应第 03 轮:工具、知识库检索、MCP 与子 Agent。
@sinabs
sinabs merged commit 7893ba6 into main Nov 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant