☰
AgentScope Java实战:知识层与工具层的架构设计与落地
2026/10/6 14:44:36 网站建设 项目流程

AgentScope Java 系列写到第三篇,前面我们已经把 Agent 本身聊通了:能说话、能按流程走、能被编排。但真到了生产环境,你会发现一个特别尴尬的问题——如果只给 Agent 一张嘴,它什么都敢说,尤其敢编答案。你问它内部系统里的订单状态,它一本正经回你一句“根据系统记录,订单已发货”,其实它根本没见过系统。你让它查产品文档里某个参数的含义,它可能凭训练数据中的记忆胡诌一段。想让 Agent 真正变成生产力,光有大脑还不够,得给它配上“书架”(知识层)和“手”(工具层)。这篇“AgentScope Java 实战 03”要解决的就是这件事:知识与工具层怎么设计、怎么落地,以及我在实际项目中踩过的那些坑。

这篇适合已经了解 AgentScope Java 基础概念的读者,尤其是正在用 Java 做 RAG 问答、Function Calling、企业内部助手这类场景的后端开发者。如果你还用过 LangChain 或者自己写过裸的 function calling,那看看 AgentScope 的做法也会有收获。

1. 先理清楚:知识层和工具层在 AgentScope 里到底管什么

1.1 一句话说清知识层与工具层的差异

很多人把知识层和工具层混在一起,觉得都是“给模型喂额外信息”。其实这两者的机制完全不同,可以打个比方:

知识层是书架。它存放的是只读的、相对稳定的信息——产品文档、FAQ、历史工单、专家经验。模型需要某个事实时,通过检索把相关片段临时取出来放到上下文里。

工具层是手。它做的是可执行、有副作用的动作——查数据库、调用下游系统、发邮件、改配置。模型自己不会执行这些操作,只能发出“我想调用这个工具,参数是这些”的请求,由框架帮你真正执行。

如果只装书架,Agent 能“知道”更多,但依然什么也做不了;如果只装手,Agent 能做事,但缺少背景知识,容易乱动。两者缺一不可。

1.2 知识与工具层挂在编排流程的哪个位置

要理解这两个层,得看 AgentScope Java 运行时的消息循环大致是怎么走的:

模型层(大脑)负责推理和生成文本;知识层在模型生成前介入,把检索结果作为额外上下文塞进 prompt;工具层在模型生成后介入,解析模型输出的 tool_calls,执行对应方法,再把执行结果作为一条工具消息喂回给模型。

也就是说,知识层不是直接压到模型输入里的,工具层也不是直接跟业务代码写在一起的。它们都是围绕“模型输入/输出”这个核心循环做扩展。

在 AgentScope Java 里,这套链路通常长这样:用户输入 → Agent 组装消息 → 查询知识库,注入相关知识 → 调用大模型 → 模型返回文本或工具调用请求 → 工具管理器执行 → 结果回填 → 再次调用模型 → 直到模型给出最终答复。

我见过很多刚开始做 Agent 的代码,把知识检索塞到一个巨大的 service 里,把工具调用写在 if-else 里,最后模型一问就乱。之所以先画这个位置图,就是想说:知识层和工具层在架构上是正交的,千万别揉成一把梭。

1.3 判断边界:什么该进书架,什么该做成手

这块是我在实际拆需求时最常被问到的:“这个数据放知识库还是做成工具?”我一般用三个问题来判断:

第一,这个信息是静态的还是动态的?静态的、低频变化的(如产品规格、操作手册、企业制度)优先进知识库。动态的、实时查询的(如库存、订单状态、账户余额)必须做成工具。

第二,这个动作是“查”还是“改”?查,且结果可以被文档化描述,进知识库;改,或产生外部副作用,必须做成工具,并且要加权限和审计。

第三,这个信息是一次性被完整消费,还是需要按相关性筛选?一个几百页的手册不可能全塞给模型,就需要切分、索引、检索,这就是知识库该干的事。一个接口一次能返回完整结果,做成工具更直接。

拿我最常做的电商售后场景举例:

  • 退货政策、保修条款 → 书架
  • 查订单物流、修改退款金额、创建售后单 → 手
  • 客服话术模板 → 书架
  • 查询用户是否在黑名单 → 手

边界清楚了,后面设计就不会颠三倒四。

2. 书架侧:知识接入、召回与记忆落盘的完整链路

2.1 书架的第一层考验:知识从什么格式进入

知识层不是简单地把一堆文件丢给 Agent。Agent 的上下文窗口有限,就算支持 200k 上下文,你也不可能每次调用都把全部公司文档塞进去。书架必须先解决“怎么进”的问题。

我处理过的知识源主要有三类。

第一类是结构化数据,比如数据库里的商品信息、问答对。这类最省事,可以直接按行转成文档片段,注意保留主键和来源字段,方便溯源。

第二类是半结构化文档,比如 Markdown、Wiki 页面。这类需要按标题层级和段落结构做切分,而不是按固定字符数硬切,否则语义会被拦腰斩断。

第三类是纯文本,比如 PDF、邮件、聊天记录。这类需要先做格式清洗,去掉页眉页脚、换行符、乱码字符,再切分。实操中很多团队把功夫全花在向量检索上,结果源文档一团糟,召回自然一塌糊涂。

我习惯在接入前定义一个统一的知识文档模型,至少包含:文档ID、标题、正文切片、元数据(作者、更新时间、来源链接)、权限标签。后续检索、引用、权限过滤全靠这套模型。

2.2 切分与向量化:召回质量的起点

知识切分没有标准答案,但有经验值。我踩过的情况是:切得太碎,比如每 100 个字一刀,检索出来的片段往往只有孤立的几句话,上下文不完整,模型回答时只能靠猜;切得太长,比如一整章一个向量,召回时语义不够聚焦,还可能因为太长把模型上下文撑爆。

比较稳妥的做法是语义边界优先、长度兜底。先把文档按 Markdown 标题、自然段等边界切块,再把单块大小控制在 300~800 token 左右;如果某段明显过长,再按句子继续拆分,并允许相邻块之间保留少量重叠,避免关键信息正好落在切缝处丢掉。

切完之后就是向量化。这里的核心是 Embedding 模型的选择。中文场景和英文场景要分开考虑,不能用通用向量模型硬扛中文业务词汇;选模型时先在你自己的数据集上抽样测一下相似度排序,不要只看榜单分数。同时,embedding 模型最好和 Agent 使用的 LLM 在语义风格上匹配,不然检索出来的“相关”结果,模型看起来并不相关。

这里还需要注意向量化服务的部署位置。Java 服务端调用 Python 侧 embedding 服务是常态,中间走 HTTP,务必做好超时和降级。如果 embedding 接口挂了,整个知识问答就直接不可用,这个风险在设计时要提前暴露出来。

2.3 召回策略:不是 TopK 越大越好

向量检索只是召回的手段,不是终点。我在项目里吃过 TopK 的亏:最初为了“不遗漏”,TopK 拉到 10,甚至 20,结果模型收到的上下文里混进大量弱相关片段,回答起来反而瞻前顾后,还疯狂消耗 token。

后来我固定了一套组合策略:

  • 先做向量召回,取候选结果 TopK = 30 左右;
  • 再做重排或过滤,用基于规则的打分或 rerank 模型,把真正的 TopN 压到 3~5 个;
  • 设定相似度阈值,低于阈值的直接丢弃,让模型坦率说“不知道”,而不是拿不相关内容硬编。

更关键的是,检索回来不要直接把原始切片拼进 prompt。每个切片要带上标题、章节路径、来源页码这些元信息,让模型知道这段内容的“出身”。我在实践里看到,带来源元信息的检索片段,回答的上下文连贯性和可信度明显更高,还方便最终回答中给用户展示引用来源。

至于重排怎么做,小项目可以用 BM25 和向量得分做加权融合,大项目再上 rerank 模型。先用简单的,别一上来就造重排服务。

2.4 记忆是书架的另一种藏书:会话记忆怎么落盘

知识库解决的是“不知道”的问题,记忆解决的是“忘了”的问题。同一个用户多轮对话里,Agent 必须能记住前面说过的话,但不能什么都长期留着。

我会把记忆分成两层:短期会话记忆,通常存在进程内或 Redis 里,过期时间就设在会话生命周期内,保存的是最近几轮消息压缩后的摘要;长期个性化记忆,存用户偏好、历史事实类信息,落库或落向量库,跨会话复用。

AgentScope Java 为这种分层记忆提供了接入点,但你要想清楚每层放什么。短期记忆别搞复杂,滚动窗口加摘要就行;长期记忆一定要带用户维度的隔离,否则 A 用户的偏好会污染 B 用户的回答,这在多租户系统里是严重事故。

另外,记忆和知识库在检索时最好不要混成一个结果集。知识库检索结果代表“事实”,记忆检索结果代表“个性化偏好”,在 prompt 里要分成不同的 section 提供给模型,模型才知道哪些是通用规章、哪些是针对这个用户的专属信息。

3. 手侧:工具注册、函数声明到调用闭环的实现细节

3.1 工具的本质:给模型一个可控的“副作用入口”

大模型本质是文本生成模型,它不具备执行能力。所谓 Function Calling,其实是模型在生成文本时,额外输出一段结构化的“函数调用意图”。AgentScope Java 要做的事情,是把这个意图和 Java 世界里真实的方法调用对应起来。

工具不是越强越好,而是越可控越好。给模型一把刀可以用来切菜,但也可能伤人。所以工具注册时就要考虑:哪些工具对哪些用户可见、哪些操作需要二次确认、哪些工具根本不该暴露给自主调用链,只能由人工触发。这个边界,注册阶段就要定死,不能等运行时再拦。

3.2 Tool 声明怎么写,模型才看得懂

模型不像人,它看不到你的 Java 方法体,只能看你提供的工具描述和 JSON Schema。所以工具声明有几个怨种点,写不好会直接导致模型瞎调或者不调。

  • name 要短且语义唯一,比如 query_stock,而不是 StockQueryServiceImpl。
  • description 要写清楚这个工具“什么时候用、什么时候不用”。我见过只写“查询库存”的,模型连用户问物流也去调库存,就是因为没写“仅当用户询问商品实时库存或数量时使用,不适用于物流信息”。
  • 参数 schema 要严格。参数名、类型、是否必填、取值范围都要写死。枚举值如果不写全,模型就会自由发挥,传一个你根本没处理过的值进来。
  • 返回值结构也要描述,最好说明返回的是 JSON 字符串还是纯文本,模型好判断怎么解析。

如果你用 AgentScope Java 的注解方式注册工具,核心工作其实是写好那个描述字符串。我经常把工具描述当成接口文档来评审,而不是随便填写,因为工具描述就是模型的“说明书”。

下面是我习惯的声明方式示意,具体注解名会随版本略有出入,机制基本一致:

@AgentTool( name = "query_stock", description = "按商品SKU查询实时库存数量。仅当用户询问某个商品是否有货、剩余多少件时使用;不适用于查询订单物流、价格和促销信息。", params = { @ToolParam(name = "skuId", type = "string", description = "商品SKU编码,形如SKU2024-011", required = true) } ) public String queryStock(String skuId) { StockInfo stock = stockService.getBySku(skuId); if (stock == null) { return "{\"found\": false}"; } return "{\"found\": true, \"stock\": " + stock.getAvailable() + "}"; }

3.3 工具调用的完整闭环:模型发话、框架跑腿、结果回填

工具调用不是一步到位的。完整闭环是:

  1. 用户提问;
  2. 模型根据工具描述判断需要调用某工具,输出 tool_calls;
  3. AgentScope Java 捕获到 tool_calls,解析参数;
  4. 框架按注册表找到对应方法并执行;
  5. 执行结果包装成 tool role 消息,重新拼进对话上下文;
  6. 模型拿到工具结果,整理成最终回复。

这里有个细节:第 4 步执行完,不能直接把原始对象返回给模型。最好统一序列化成字符串或 JSON,同时包含成功/失败状态、错误信息。如果工具执行抛异常了,别让异常把整个 Agent 循环搞崩,把异常信息折叠成一句“工具调用失败,原因:xxx”,交给模型决定是重试、换个工具、还是如实告诉用户失败。

如果一次模型返回里包含多个工具调用,比如同时查库存和查价格,框架要考虑是串行还是并行执行。串行简单可控,并行快但要小心共享状态。我一般默认串行,只有确认工具间完全无依赖时才允许并行。

3.4 工具执行的治理:超时、幂等、权限、审计

工具层的风控,比功能本身更重要。我把这一块称为“工具治理”,这几条是我做任何 Agent 工具层都强制要有的:

  • 超时。每个工具执行必须设置超时时间,建议默认 3~5 秒。Agent 是面向用户的实时交互,一个下游接口慢 30 秒,体验直接毁了。超时后要能返回可理解的错误,而不是空等。
  • 幂等。工具若会产生副作用(改库、发单、转账),必须让下游支持幂等键。模型可能会因为上游重试而重复调用同一个工具,如果没有幂等,用户就会被重复扣款、重复下单。这种事故我见过不止一次。
  • 权限。工具注册表里每个工具标注“用户级”“管理员级”“系统级”,Agent 在选择工具时要根据当前会话主体做过滤,不能让普通用户通过 Agent 调用管理员工具。
  • 审计。所有工具调用都记录入参、出参、耗时、由哪个用户触发、由模型哪一次决策触发。出了事可以回溯,不然连锅都找不到。

我用一个简单的 HTTP 风格表格总结治理维度,方便你对照检查自己的工具层:

维度要求常见事故
超时3~5秒强制中断下游慢拖垮整个Agent
幂等带幂等键重试重复下单、重复扣款
权限按会话主体过滤越权调用管理工具
审计全量留痕可追溯事故无法定位
限流按用户/工具限速恶意刷调用耗尽资源

4. Java 工程落地:用 Spring 管理工具,用本地检索库搭知识层

4.1 知识层选型:从 Lucene 到专门向量库,各取所需

Java 后端做知识库,选型比 Python 生态更“重”,但可选路径也很明确。

小规模、单机、想快速跑通,用 Lucene 或者带向量检索能力的本地索引最合适。Lucene 的 BM25 做关键词召回很成熟,近几个版本也支持向量字段,对几十万级别的文档量完全够用,部署还省心,不用额外引一个服务。

中等规模、已有 ES 集群,直接把文档索引和向量索引都放在 Elasticsearch 里。ES 的向量检索能力虽然 HNSW 性能一般,但胜在复用基础设施、查询语法统一。

大规模、高 QPS、向量检索是核心路径,上 Milvus、Qdrant 这类专用向量库,或者云厂商的向量检索服务。此时工程复杂度会明显上升:集合管理、标量过滤、索引构建、多副本,每一项都要有人专门盯。

我给一个保守的选择依据:

场景推荐方案理由
Demo、个人项目Lucene / 本地向量文件零运维、直接内嵌
中小团队内网知识库Elasticsearch复用已有基础设施
高并发对外智能助手专用向量库性能与扩展性更稳

另外,如果文档量很小(几千条 FAQ 以下),别硬上向量库。直接一个 HashMap 加载到内存里、用关键词匹配和简单的 TF-IDF,效果甚至更好,因为你可以在召回后直接用规则做精确匹配。玩具场景不要用战术的勤奋掩盖战略的懒惰。

4.2 工具侧工程化:把 Agent 工具注册到 Spring 容器

在 Java 工程里,工具不应该散落在乱七八糟的静态方法里。我推荐的做法是:把工具按业务域拆成不同的 ToolProvider Bean,统一交给 AgentScope 的工具管理器扫描注册。

@Component public class OrderToolProvider { private final OrderService orderService; public OrderToolProvider(OrderService orderService) { this.orderService = orderService; } @AgentTool(name = "query_order_status", description = "...") public String queryOrderStatus(@ToolParam(name = "orderId", ...) String orderId) { Order order = orderService.getOrder(orderId); // 返回统一JSON } }

这样做的好处很明显:工具能像普通 Spring Bean 一样依赖注入,数据库连接、配置中心、Redis 都直接可用;工具生命周期由容器管理,方便加 AOP 做审计和限流;测试时可以注入 mock,不真的调下游。

工具注册表的设计我建议至少包含:工具名、方法句柄、参数 Schema、工具类型(只读/写操作)、需要的权限级别、超时时间。运行时按会话主体的权限做过滤,而不是把所有工具一股脑都暴露给模型。

4.3 一个最小可跑的骨架:Agent + 知识检索工具 + 业务工具

把知识层和工具层拼起来,最小的骨架大概是这样:

public class AssistantDemo { public static void main(String[] args) { LLM model = LLMs.getModel("your-model-config"); // 1. 知识检索,作为一个普通工具暴露给模型 ToolManager toolManager = new ToolManager(); toolManager.register(new KnowledgeSearchTool(ragService, 3)); // 2. 业务工具 toolManager.register(new OrderToolProvider(orderService)); // 3. 组装Agent,这里以ReActAgent示例 ReActAgent agent = ReActAgent.builder() .model(model) .tools(toolManager) .maxIterations(5) .build(); // 4. 提问 AgentResponse response = agent.run("我们公司投影仪的保修期是多久?另外 SKU2024-011 现在有货吗?"); System.out.println(response.getContent()); } }

这个骨架里,知识检索被包装成工具,是因为模型可能需要在回答过程中按需检索。有的场景你也可以把知识检索放在 agent 消息组装阶段主动注入,区别在于:主动注入适合“每轮都必须先查”,按需检索适合“模型自己判断要不要查”。

我见过很多初版代码把知识检索写死在下游服务里,模型只能被动接受。真正留出“模型自主决定检索时机”之后,逻辑反而更贴近真实使用方式——用户没说之前,模型不一定需要先翻书。

5. 一个跑通的生产级例子:文档问答加实时库存查询

5.1 需求拆解:一次对话里同时用到书架和手

我拿一个相对完整的需求演示:用户问“我们公司新项目的投影仪保修期多久?这台投影仪有货吗?能给我报个价吗?”这个场景里,保修期属于知识库;库存和价格属于实时业务工具。如果只装书架,第三个问题答不了;如果只装手,第一个问题没有工具能答,模型只能凭训练记忆瞎编。

这种“知识 + 工具混合提问”在真实业务里非常常见。Agent 需要先检索内部文档定位保修规则,再调用库存接口查实时数量,最后调用报价工具计算价格。一个正确的实现应该对这三种请求分别处理。

5.2 知识检索工具的返回值设计

知识检索工具我通常会返回包含多个候选片段的结构化文本,而不是只返回一个最佳匹配:

{ "query": "投影仪保修期", "hits": [ { "score": 0.92, "title": "商用投影仪售后政策 2024版", "content": "所有商用投影仪整机保修三年,核心部件保修五年...", "source": "https://wiki.internal/projector/warranty" } ] }

返回给模型时要强调:如果检索结果不能直接回答问题,必须明确告知用户“未在资料中找到相关信息”,不允许编造。这个约束写在系统提示词里还不够,最好在知识检索工具的 description 里也写一遍,双重约束才有效果。

我踩过的一个教训是:知识检索结果里只有 content 没有 title 和 source,模型就算答对了,也不知道出处,引用来源全靠瞎编。加上了明确的 title 和 source 字段之后,模型回答时会把出处说清楚。

5.3 结合工具的完整对话流

实际跑出来的一次对话流大致如下:

  1. 用户提问;
  2. 模型判断需要知识检索,发起工具调用 search_knowledge_base(query: "投影仪保修期");
  3. 框架执行,返回保修政策片段;
  4. 模型继续判断需要查询库存,发起 query_stock(skuId: "SKU2024-011");
  5. 框架执行,返回 found: true, stock: 36;
  6. 模型继续调用 quote_price(skuId: "SKU2024-011");
  7. 框架执行,返回价格;
  8. 模型综合所有信息,生成最终答复。

如果你用 AgentScope Java 的调试工具或自行打印消息序列,会看到这一串 msg 交替:user → assistant(tool_call) → tool → assistant(tool_call) → tool → assistant(final)。只要能看到这种模式,说明工具闭环是通的。

5.4 这类场景的收益和注意点

内容安全再强调一下:企业内部知识往往有保密等级,知识检索工具在执行时必须过滤当前用户无权限的文档。我经历过一次事故,低权限用户通过 Agent 检索到了内部高权限政策文档内容,直接查了聊天记录才发现忘记做权限过滤。

技术上的收益很直接:不再需要为每个问答场景单独维护庞大的提示词模板;知识更新时只需更新索引,不用改代码;新增业务操作只需新增一个工具方法,不改 Agent 主流程。这个收益需要跑过一个完整迭代周期才会真正体会到。

6. 实测清单:知识层和工具层最容易翻车的六个环节

6.1 工具描述写得太“文科”,模型就会乱摸手

最初我把工具描述写得很像人话,比如“这个工具可以查询订单如果你想知道用户买了什么的话”。模型根本抓不住边界,用户问一句“我什么时候到货”,它也会去调订单查询工具。后来改成“仅当用户询问订单配送状态时调用;不得用于售后投诉”,误调率立刻降下来了。

工具描述的正确格式应该是:这个工具做什么 + 什么时候调用 + 什么时候不调用 + 关键参数解释。行业里很多团队已经开始用“触发条件 + 非触发条件”的方式写工具描述,这对模型消歧义是有效的,值得借鉴。

6.2 切分不合理,向量检索召回落入“近而不准”

知识切分直接决定召回质量。我调试过一个客户案例:他们把产品文档按每 200 个字符固定切分,结果很多关键知识点被截成两半,检索时还总能命中,但下半句是上一段的尾巴,模型拿到的片段不完整,回答自然差。后来改为按章节段落切分,并把切分单元设为 500 token、重叠 50 token,召回质量立刻上了两个台阶。

我的建议是:任何一次“检索质量差”的抱怨,都先去看源文档的切分和清洗,不要一上来就换向量模型。向量模型再强,也救不了烂掉的切分结果。

6.3 上下文被知识堆满,成本和效果双输

TopK 太大、召回片段太长,都会把上下文字数顶得非常高。我见到不少人被账单吓到过,明明只是简单问答,一次请求却输入了好几万 token。

解决办法:知识检索结果按相关性排序后只取前三;每个片段在拼入 prompt 前做压缩,只保留命中句和前后少量上下文;重排阶段如果关键句重复出现,只保留信息量最高的那一个。分层记忆也只放必要信息,长期记忆经过摘要之后再入库,而不是把原始会话一字不差存进去。

下文提示词里的知识 section 也不该和普通历史消息混在一起,我会专门用明显的标记区分“以下是检索到的资料片段”,让模型知道这些内容属于资料而不是用户原话。

6.4 并发与重复执行:对模型的不确定性要有预期

模型输出并不稳定,同样的 prompt 可能这次输出 tool_call,下次直接输出一段“我好帮你查到”的假话。我们在生产里遇到的另一个问题是模型把同一次提问拆出多个工具调用,但参数完全一样;或者因为框架层面超时重试,把同一个写操作执行了两次。

应对办法:工具注册时预设 idempotencyKey,每次工具调用的会话生成唯一键,下游接口支持基于该键的幂等;重复参数的工具调用在框架层做去重,相同工具+相同参数的并发调用直接合并;对写操作工具强制要求人工确认,不允许模型自主二次尝试。

6.5 日志链路怎么搭:观测模型到底要做什么

工具层一旦复杂,定位问题就成了一场灾难。我强烈建议从上线第一天就把链路日志打全:每次模型调用记录 tool_calls 原始输出;每次工具执行记录入参出参、耗时、是否幂等命中;每次知识检索记录查询词、召回列表、最终选用的片段。

我把这些日志统一打到独立的索引里,用 traceId 贯穿一次对话。出了问题,先看模型想调什么,再看框架实际执行了什么,最后看工具返回了什么。三步一对,80% 的问题都能定位。

6.6 版本升级与工具契约:别让新模型毁掉旧方法

框架和模型都会升级。模型切换后,工具描述不一定还适用于新模型的 function calling 格式。我在一次模型升级后发现,新模型对 description 里“绝不”这类否定词的理解偏弱,导致不该调的工具频繁被调。说明工具描述也需要针对模型做回归测试。

每次升级模型或 AgentScope 版本,都要跑一遍工具调用回归用例:固定 50 条问题,人工判定每条是否应调用工具、参数是否正确、最终回答是否合理。这个是苦功夫,但能避免线上翻车。

我对知识与工具层最重要的一个体会是:Agent 的能力上限,其实不是你模型选得多强,而是你的“书架”整理得有多清晰、你的“手”管得有多稳。工具乱、知识脏,再好的模型也只是个反应快但缺乏常识的机器人。下一篇我可能会写编排层,讲讲多个带手带书架的 Agent 怎么合作——那又是另一个值得掰开揉碎的话题。

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

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

立即咨询