很多 Agent 项目跑不起来,不是因为模型不够聪明,而是因为没有"手"和"书架"——模型很会说话,但既不能真的去查数据、改状态,也没有一份可以随时查阅的内部资料库,结果一问就露馅。之前用 AgentScope Java 搭好了能跑通对话的 Agent 基础骨架,但真要往业务上放,卡住的往往是这一层:知识与工具层。这篇实战 03,我把在 AgentScope Java 里给 Agent 装"手"(工具调用)和"书架"(知识检索)的完整做法、落地细节和踩坑记录整理出来,适合已经把 AgentScope Java 基础跑通、准备接真实业务的开发者,也适合刚接触 Agent 开发、想知道工具和知识到底怎么落到代码里的朋友。
1. 先搞清楚:工具层和知识层到底在解决什么问题
1.1 工具层:把"会说话"变成"能办事"
先说工具层。大模型本质是一个文本生成器,你给它一段 prompt,它给你一段回复。但业务系统要求的是什么?是"帮我查一下订单状态""把这个工单状态改成已处理""根据报价规则算一下运费"。这些操作不可能靠模型吐一段文字就完成——它没有权限,也不应该让它有直接操作数据库的权限。
工具层就是在这中间架一座桥:模型负责任务理解与决策,输出"我想调用哪个工具、传什么参数",真正执行的是你写在 Java 类里的方法。这个机制在行业内通常叫 Function Calling / Tool Calling,AgentScope Java 天然支持,我们只需要把方法声明清楚,框架负责让模型"看到"这些工具,并在模型决定调用时,路由到对应的方法上执行。
我见过不少第一次接触 Agent 开发的同事,总觉得工具层很高深。其实说白了就是三件事:把你的方法"注册"给模型看、模型按需选择、框架帮你执行并把结果喂回给模型。难点从来不在概念上,而在"如何把方法声明得让模型看得懂、用得上"。
1.2 知识层:解决"一本正经地胡说八道"
知识层解决的问题更朴素。模型的训练数据是有截止时间的,它不可能知道你公司内部的产品手册、报价规则、售后流程。你直接问它"我们的退款政策是什么",它大概率会编一个听起来很像那么回事的答案——这就是所谓的幻觉。
知识层的标准做法是 RAG(检索增强生成):先把资料切分成片段,做向量化存入知识库;用户提问时,先从知识库检索出相关内容,再把这些内容拼进 prompt 给模型参考。相当于给模型配了一个可随时翻阅的"书架",问什么先翻书,翻到了再回答,不凭记忆硬编。
不过这里要提醒一句:"书架"不只是静态文档。一个真正能用的 Agent,知识来源至少有三类:静态知识库(产品手册、规章制度)、结构化业务数据(订单、库存)、动态对话记忆(用户之前说过什么、偏好什么)。这三类在 AgentScope Java 里接入方式不太一样,下一节我会细讲。
1.3 两个层在 AgentScope Java 里的整体位置
我觉得先把这两个层在 Agent 运行链路里的位置画清楚,后面写代码才不会晕。一次完整的 Agent 请求大概是这样的:
用户提问 → 会话记忆整理 → 意图理解 → 知识检索(如果需要) → 模型决策(用哪个工具?要不要查资料?) → 执行工具/读取检索结果 → 模型组织最终回复 → 返回给用户
工具层和知识层的位置在"意图理解之后、模型决策前后"。知识检索为模型提供事实依据,工具调用为模型提供执行能力。在 AgentScope Java 的组件结构里,工具会注册到工具管理器(Tools),知识库和记忆则对接上下文组装器——最终它们都会被拼装成模型可见的内容或可调用的函数列表。
理解了位置,接下来就可以动手了。
2. 装手:从普通 Java 方法到可被 Agent 调用的工具
2.1 最省事的声明方式:在方法上加描述
在 AgentScope Java 里注册一个工具,最直观的方式是把一个普通 Java 方法标记为"可被 Agent 调用的工具"。
我这边项目经验的写法大概是这样的(不同版本注解名可能稍有差异,思路是通用的):
public class LogisticsTools { @Tool( name = "calculate_freight", description = "根据重量和距离计算物流运费,只用于国内快递报价。", paramDescription = { "货物重量,单位千克,支持小数。", "配送距离,单位公里,整数即可。" } ) public String calculateFreight(double weightKg, int distanceKm) { double base = 8.0; double total = base + weightKg * 1.5 + distanceKm * 0.02; return String.format("运费计算结果:%.2f 元", total); } }然后把这个工具类注册到 Agent 上:
Agent agent = Agent.builder() .model(model) .tools(new LogisticsTools()) .build();核心就这几行。AgentScope Java 会自动把你标记过的方法提取出来,生成模型可读的工具清单,并处理参数映射和调用路由。
这里有个主要建议:工具方法尽量用基础类型做参数(String、int、double 这类),返回结果也尽量用简明文本。原因后文会讲——模型对复杂对象结构的天生弱视,远比你想象的严重。
2.2 工具描述就是给模型的说明书,决定它会不会用
我踩过最直接的坑:工具声明得没问题,签名也没错,但 Agent 就是死活不调用,或者调用了错误的工具。查到最后,问题出在"描述"上。
模型决定是否调用一个工具,依据的是你写的 name 和 description。它先看描述,再看参数。描述写得含糊,模型就只能靠猜。
举个反面案例。有个工具方法命名为calcPrice,描述写的是"计算价格"。听起来没毛病,但 Agent 面对两个工具时——一个是calcPrice(计算运费),一个是calcTax(计算税费),用户问"运费多少钱",模型犹豫半天选了calcTax。
改成下面这样之后,问题立刻消失:
@Tool( name = "calc_freight_price", description = "计算国内快递运费。当用户询问寄件费用、运费、快递价格时使用。需要提供货物重量(千克)和运距(公里)。" )核心原则是:描述要回答三个问题——这个工具做什么、什么时候该用、需要什么关键输入。不要写废话,但要把触发条件写清楚。
2.3 从用户提问到工具返回,完整链路
工具调用的完整链路,我拆给你看:
用户问:"从北京寄 3 公斤的东西到上海,多少钱?"
第一步:AgentScope Java 把用户问题、历史记录、系统 prompt 一起交给模型,同时附上工具清单(你注册的那些方法)。
第二步:模型判断"这个问题需要计算,应该调用calc_freight_price",于是返回一个结构化的工具调用请求,包含工具名和参数值(weightKg=3.0, distanceKm=1200 左右,具体由模型从上下文推理得出)。
第三步:框架拦截到这个请求,反射调用你注册的 Java 方法,拿到返回结果。
第四步:框架把工具返回结果拼接到原始对话中,再次交给模型,让模型基于这个结果组织自然语言回复。
这里面有个细节很多人容易忽略:工具方法的返回值会作为"模型看到的文本"再次进入模型。所以返回文本的格式要尽量利于模型理解。我习惯在返回结果里直接带结论,而不是丢一堆 JSON——模型从 JSON 里提炼数字再算一遍,出错概率会高很多。
提示:如果工具返回的是结构化的业务对象(订单信息、商品列表),建议在方法内部先把它转成一段简明的文本再返回。模型更擅长读文字,而不是解析结构。
3. 装书架:知识库接入与检索的落地细节
3.1 知识、记忆、上下文:三个容易混的东西
在接知识库之前,我强烈建议先把三个概念分清,因为它们在代码里是三种不同的处理方式。
静态知识:公司手册、操作规范这类长期不变的资料。对应的是"知识库 + 向量检索",按需取用。
动态记忆:用户的历史偏好、上次聊到哪了、最近几次对话摘要。对应的是"记忆组件"。AgentScope Java 里可以把最近几轮对话自动带上,也可以把关键信息抽成记忆片段。
上下文:当前这一次请求最终组装给模型的所有材料,包括用户问题、切出来的记忆、检索到的知识片段、工具说明和结果。
我曾经把这三样全塞进同一个向量库,结果用户只是换了个说法问同一个问题,Agent 检索出来的却是完全不相关的历史对话。后来才把静态资料和对话记忆分库存储,问题才消停。
在 AgentScope Java 里,这三样东西分别由不同的组件管理:知识库组件负责静态资料的检索,记忆组件负责会话历史的存取,上下文组装则由 Agent 的内部逻辑完成。所以你接"书架"的时候,先想清楚接的是哪种知识,别一股脑全塞进去。
3.2 最小可用的知识库:切分、向量化、检索
一个最小可用的知识库,流程是固定的:资料切分 → 文本向量化 → 存入向量数据库 → 检索召回。
在 AgentScope Java 里,你可以自己组织这条链路,也可以直接接入现成的向量库组件。常见的组合是:Embedding 模型负责向量化,向量数据库(如 Milvus、Chroma 或你内部已有的引擎)负责存储和检索。
这里我只讲一个最容易被忽视的参数:切分大小。
我一开始图省事,把整份产品文档切成 64 个字符的碎片,想着这样检索会更精准。实际跑起来发现,检索召回的内容经常是"残肢断臂"——一句完整的话被拦腰截断,模型拿着半句话根本没法组织回答。
后来改成按语义段落切分:一个段落或者几条相关句子组成一个片段(我这边实践下来 500~1000 字比较稳妥),检索召回时也尽量按片段返回而不是按单句返回。模型拿到的上下文完整了,回答质量立刻上一个台阶。
3.3 检索参数:topK 和相似度阈值不是越大越好
知识库检索有两个参数最影响效果:topK(取几条结果)和相似度阈值(低于多少分不return)。
我这边实测的结论是:topK 不要一味调大。topK=3 时,Agent 专注于最相关的资料,回答准确率最高;调到 topK=8 以后,大量低相关的片段混进来,模型反而开始"左右为难",甚至引用错误内容。
相似度阈值也一样。阈值设太低,垃圾内容全进来;设太高,该召回的内容又漏了。我用过一阵 0.7 的阈值,后来发现不同 Embedding 模型的分数尺度差异很大,换模型就得重新标定。稳妥的做法是:离线准备几十个真实问题,跑一遍检索,看召回的片段是不是人工也认为相关,再定阈值。
提示:知识库上线后不是一劳永逸的。把用户实际问过的、检索效果差的问题攒起来,定期重新切分、调参,比反复换模型有效得多。
4. 手和书架协同:一个"查规则再算价"的完整场景
4.1 场景设定:让 Agent 根据内部费率表计算运费
工具和知识单独都能跑,但真正体现价值的,是它们协同工作的时候。我拿近期做的一个物流咨询 Agent 举例。
需求是这样:客户会问"北京到上海 3 公斤多少钱""我们发的这批货要走 500 公里,首重多少"。报价规则存放在一份内部费率文档里,有首重价、续重价、按距离的分档系数。这些规则如果写死在代码里,改一次费率就得发一次版;如果让模型靠训练知识回答,那纯属碰运气。
于是设计成:费率规则放知识库(书架),运费计算逻辑做成工具(手)。Agent 先检索知识库拿费率规则,再调用工具完成计算,最后组织答案。
4.2 一次真实请求的链路拆解
用户提问:"北京到上海 3 公斤,多少钱?"
AgentScope Java 内部大概走了这么几步:
第一步,知识检索。框架把"北京到上海 3 公斤 运费"作为查询,在知识库里检索到关于"计费规则首重与续重说明"和"北京-上海属于华东区,距离系数 1.2"的片段。
第二步,材料拼接。检索到的费率片段和工具清单一起进入模型输入。
第三步,模型决策。模型发现需要计算,决定调用calc_freight_price工具,参数为 weightKg=3.0、distanceKm=1200(它从知识片段里推理出这个值)。
第四步,工具执行。你的 Java 方法按规则算出运费。
第五步,最终回复。模型拿到工具计算结果,结合检索到的费率解释,回复用户:"您好,北京到上海 3 公斤运费为 35.6 元,其中首重 8 元,续重按每公斤 1.5 元计算,华东区距离系数 1.2。"
整个过程里,工具负责精确计算,知识库负责提供计算依据,模型负责把两者翻译成用户能懂的话。哪个环节负责什么,分得明明白白。
4.3 为什么这样设计,而不是把规则全塞进 Prompt
可能有朋友会问:费率规则也不多,直接写进 system prompt 不就行了吗?
短期内可以,但有几个实际问题:规则一多 prompt 会爆炸,每次请求都传全量规则,token 成本和响应时间都受不了;规则变了还要改代码、重启,这不是 Agent 该有的工作方式。
知识库的好处在于按需加载:用户问上海,就只取华东区的规则;用户问北京,就只取华北区的规则。工具的好处在于计算准确:模型天生不擅长算术,但把计算交给确定性的 Java 方法,结果一定是准的。
这就是所谓的手和书架的分工——书架负责"知道规则",手负责"执行计算",大脑(模型)负责"协调和解说"。
5. 实战排错:在这个层上最容易踩的几个坑
5.1 工具描述含糊,Agent 就是不用或者选错
前面提到过,描述是模型判断的唯一依据。我再补充一个排查方法:如果 Agent 不调用你预期的工具,先把工具描述打印出来,站在"一个不知道你代码逻辑的陌生人"的角度看一遍,看它能不能根据描述判断出"这个工具什么时候用、怎么用"。
我碰到过一个典型case:工具方法内部逻辑完全正确,但描述里没写"单位"——重量是千克还是克,模型搞不清楚,于是不传参数或者传错单位。在描述里加上"单位千克",调用准确率立刻上去了。
5.2 知识库命中率低,别急着换 Embedding 模型
很多朋友一发现检索结果不对,第一反应是换更贵的 Embedding 模型。我的经验是:先看切分,再看查询,最后才是换模型。
检查切分是否把完整语义切碎了;检查查询词是否需要改写(比如用户问"运费多少",知识库里写的是"资费标准",检索匹配不好可以试着在检索前做查询改写或同义扩展);这些都没问题,才轮到换模型。
有一回我调了一整天的向量模型,最后发现是文档切分工具把表格数据切得面目全非,检索召回的内容全是表格碎片。修复切分策略后,命中率直接翻倍。
5.3 工具方法别持有可变状态,并发场景下会翻车
如果工具方法内部使用成员变量保存状态(比如记录上一次调用的结果),在并发场景下会出现串数据的问题——用户 A 的请求把用户 B 的数据覆盖了。
AgentScope Java 的 Agent 服务部署后,工具实例是可能被多线程复用的。工具方法应该是无状态的:参数全从方法入参来,结果返回后不保存任何中间状态。如果确实需要状态(比如记录上下文),把它放到 Agent 的会话记忆里,而不是工具类的成员变量里。
5.4 一定要能看清 Agent 的"内心戏"
最后这条建议可能最实用:Agent 开发最大的难点是"看不见"。模型为什么选了这个工具?知识库到底检索到了什么?工具返回了什么?整个过程对你来说就是个黑盒,出了问题根本无从排查。
所以我从第一个 Agent 项目开始就养成了一个习惯:把 AgentScope Java 的调试输出打开,把模型原始请求、工具调用列表、工具返回结果、最终回复全部打印到日志里。这不是偷懒,而是必须——没有这些日志,你连"是模型决策错了,还是知识库检索错了,还是工具算错了"都分不清。
还有一个辅助技巧:单独写一个测试入口,不经过 Agent,直接手动调用工具方法和知识检索方法,先确认"书架里有货、手能干活",再接上 Agent 联调。这样能把问题快速定位在"组件本身"还是"模型决策"。
说实话,把知识与工具层真正做扎实之后,Agent 才算从"demo 玩具"变成了"能上生产的工具"。我现在的体会是:模型这块大家用的都差不多,真正拉开差距的,恰恰是工具声明得清不清楚、知识库切分得合不合理、日志埋得够不够细这些"笨功夫"。最后再分享一个小技巧:每次新增工具或知识库内容,先用一套固定的测试问题跑一遍,把结果存档,下次改了东西再跑一遍对比。这套回归测试看着土,但能帮你兜住九成以上的低级回归问题。