我不打算从“AgentScope 是什么”这种教科书定义开始——能点进这个标题的人,多半已经在动手写了。这篇是 AgentScope Java 实战系列的第三篇。前两篇我们搞定了 Agent 的“大脑”基础结构(模型接入、消息协议、多轮对话链路),这次要解决的是一件更实在的事:怎么让 Agent 不只是“会聊天”,而是真正“会干活”。干活靠什么?靠手,也靠书架。手是工具(Tool),书架是知识(Knowledge)。在这套比喻里,AgentScope Java 的知识与工具层,就是给 Agent 装上这两样东西的地方。
这篇内容我会围绕三层展开讲:为什么知识与工具层要分开设计、AgentScope Java 里工具注册和知识检索具体怎么写、以及我在实际项目中踩过的坑和排查经验。如果你已经跑通了前面两篇的基础对话流程,这篇跟着敲完,你就能得到一个能调用外部 API、能基于自有文档回答问题的完整 Agent 雏形。
1. 先搞清楚:知识与工具层到底管什么
1.1 “手”和“书架”分别解决什么问题
在 Agent 这个领域,模型本身只负责“思考”,不负责“执行”和“记忆外的事实”。如果你让大模型算一笔复杂的账、查一个实时的天气、翻一份公司内部制度文档,它要么瞎编,要么说“我不知道”。知识与工具层就是来解决这两个短板的。
工具层解决的是“动作”问题。Agent 判断当前需要执行一个外部操作时(比如查数据库、调用接口、发邮件、执行一段计算),通过工具层把意图翻译成真实的函数调用,再把结果回传给模型继续推理。没有工具层,Agent 就是一个只会纸上谈兵的清谈客。知识层解决的是“事实”问题。模型训练数据有截止时间,也不包含你私有领域的内容。通过知识层把文档切片、向量化、存入检索库,在对话时先做相似度检索,把相关知识片段拼进提示词上下文,模型回答就有了依据。没有知识层,Agent 就是个没有常识积累的健忘者。
这两层在 AgentScope Java 里是作为 Agent 的独立组件存在的。在官方设计里,你可以分别往 Agent 上挂载 Tool 列表和 Knowledge 列表,Agent 运行时统一调度。关键词:统一调度。这也是在代码里设计和在文档里看图最大的区别——你要理解 Agent 拿到一个问题后,是在什么样的循环机制里去使用这些组件的。
1.2 AgentScope Java 的运行时循环:ReAct 模式
AgentScope Java 的 Agent 内核遵循的是业界主流的 ReAct 模式(Reasoning + Acting)。简单说,模型在每一轮推理中会经历一个循环:
- 观察:接收用户的 query,以及当前已有的对话历史。
- 推理:模型判断自己是否需要调用工具、是否需要查知识库,然后输出一个结构化的决策(比如“我要调用 get_weather 这个工具,参数是北京”)。
- 行动:Agent 运行时解析模型的决策,执行工具调用或知识检索。
- 观察结果:把工具返回的结果或知识检索到的片段作为新的消息,回填给模型。
- 再推理:模型结合回填内容,生成面向用户的最终回答,或者继续发出下一轮工具调用。
这个循环会一直持续到模型认为信息已经足够,给出最终答复为止。你的知识和工具在这套循环中的角色是“中间件”:它们不决定 Agent 说什么,但决定 Agent 能“看到”什么、能“做到”什么。
理解这个循环至关重要。我见过不少人一上来就写一堆工具方法,结果 Agent 根本不会主动调用,原因就是没理解 ReAct 循环里模型需要看到“工具的使用说明书”,而不是只看到“工具的实现代码”。工具层和知识层的本质,是给模型提供“接口契约”和“检索入口”,而不是给 Java 工程师提供一堆函数。
2. 工具层实现:怎么给 Agent 装上“手”
2.1 工具注册:从 Java 方法到模型可见的“说明书”
在 AgentScope Java 里,工具不是随便写个 public 方法就能被调用的。它需要经过一道“注册”工序,把我们熟悉的 Java 方法转换成模型可以理解的结构化描述。这个描述通常包括三件事:工具名(tool name)、功能描述(description)、参数结构(parameters schema)。
先看一个最基础的工具定义。假设我们要给 Agent 一个“查询城市天气”的工具:
public class WeatherTool { @AgentTool( name = "get_weather", description = "查询指定城市的实时天气情况,当用户询问天气时必须调用此工具", parameters = { @ToolParam(name = "city", type = String.class, description = "城市名称,如:北京、上海", required = true) } ) public String getWeather(String city) { // 这里实际去调用天气 API,或者查缓存 return "北京今日晴,气温 22℃~31℃,风力 3 级"; } }这里用了注解标注,框架在 Agent 启动时会扫描这些注解,自动生成 JSON Schema 并注入到模型的系统提示词里。模型看到的不是这段 Java 代码,而是类似这样的一份 JSON 描述(简化版):
{ "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的实时天气情况,当用户询问天气时必须调用此工具", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,如:北京、上海" } }, "required": ["city"] } } }模型看到这份 JSON 后,当用户说“北京明天需要带伞吗”,模型就会在推理中输出类似“调用工具 get_weather,参数 city=北京”的结构化指令,Agent 运行时解析并反射调用你的 getWeather() 方法,拿到返回值后拼接成一条工具结果消息,再次交给模型。
这里有几个细节要单独拎出来说:
第一,description 是给模型看的,不是给程序员看的。很多团队写工具描述敷衍了事,写个“查询天气”四个字。但模型是靠描述来决定什么时候调用工具的,描述越明确,调用率越准。我总结的写法是:触发条件 + 工具能力 + 示例。比如“当用户询问任何城市的当前天气或未来天气预报时,必须调用此工具获取实时数据,如:‘北京天气怎么样’、‘上海明天会不会下雨’”。模型不是人,它不会脑补你的意图,你得把触发场景写透。
第二,参数描述里要写明格式要求。模型会根据工具描述生成参数值。如果参数要求是“YYYY-MM-DD”格式的日期,你不在描述里写清楚,模型很可能生成“2025-01-15”之外的格式,比如“1月15日”或“今天”,导致你的工具解析失败。这就是典型的“模型与工程之间的契约摩擦”,需要用参数描述来消除。
第三,编码问题。Java 方法签名里的中文描述、中文返回值,在转成 JSON Schema 和回填上下文时都需要确认框架使用的是什么字符集。我在本地实测中遇到过框架默认设置下中文参数被转成乱码的情况,解决办法是在 Agent 配置里显式指定 UTF-8 的消息编码。这个问题在后续会细说。
2.2 工具执行流程:Agent 怎么调用你的方法
模型输出“我要调用工具 get_weather,city 参数值为北京”这只是一种中间指令,Agent 运行时需要把它“翻译”成真实的 Java 方法调用。AgentScope Java 的执行流程大致是这样的:
- 模型返回一个带有工具调用块(function call block)的消息,内容包含工具名和参数 JSON。
- 框架根据工具名在注册表里查找对应的 Java 方法。
- 框架用 JSON 反序列化工具把参数 JSON 转换成 Java 方法所需的类型。这里涉及类型映射,如果 Schema 里定义的是 string 而你的方法参数是 int,框架会尝试转换。转换失败时会抛出工具执行异常。
- 框架通过反射调用你的方法,捕获返回值。
- 返回值被包装成一条工具消息,标记上对应的工具调用 ID,追加到消息序列中。
- Agent 带着新的消息序列再次调用模型,让模型基于工具结果生成后续内容。
这个流程里,最容易出问题的就是第三步“类型映射”。我遇到过最典型的一个坑:工具定义参数是 Integer 类型,模型在生成参数时给了 "temperature" : "20"(带引号的字符串),框架的 JSON 反序列化默认抛异常。解决思路一是把参数描述写清楚,二是给工具方法增加容错入参,比如用 String 接收再进行内部转换,或者自定义反序列化器。后面“常见问题”部分我会展开讲。
顺带一提,AgentScope Java 的工具调用是同步的。也就是说,Agent 循环中每发起一次工具调用,都要等结果返回后才进入下一轮推理。对于单机 demo 这没问题,但如果你要接入外部 API 且接口响应很慢(比如超过 30 秒),整个 Agent 的响应时间会很难看。我现在的做法是:把耗时的工具方法放到独立线程池里异步执行,通过 CompletableFuture 返回结果,这样工具“执行中”的等待不会阻塞框架主线程。不过要注意,AgentScope Java 官方对异步工具调用是否完全兼容,取决于版本,需要你自己在目标版本里实测。
2.3 多工具管理与选择策略
单个工具好说,当你有十几个、几十个工具时,怎么让模型在适当的时候选对工具,就成了核心问题。这个问题的本质,还是在描述层面做文章。
我在项目里给 Agent 挂了十几个工具后,发现了两个现象:
现象一:工具越多,模型越容易“挑花眼”。有一次我同时注册了 get_weather、search_hotel、book_room 三个工具,用户问“北京今天冷吗”,模型居然同时调用了 get_weather 和 search_hotel。原因是 search_hotel 的描述里写了“包含地理位置和天气信息”,触发条件写得过于宽泛,模型被误导了。
现象二:工具名或描述相似度高,模型容易选错。比如 get_room_price 和 gen_room_price_quote 这两个工具,模型经常混淆。
解决思路有两个方向。一个是调整描述粒度,把每个工具的触发场景写细,工具之间的边界写清楚。另一个是分组/命名空间。AgentScope Java 在较新版本里支持工具分组(Tool Group),可以把一组相关工具作为一个整体注册给 Agent,比如“酒店服务工具组”、“天气工具组”,模型先决定调哪组,再在组内选具体工具。这相当于多了一层路由,能显著降低误调用率。
还有一个工程小技巧:给工具调用记录加一层日志。排查时最关键的线索就是“模型当时为什么选择了这个工具”。我在项目里会专门打印一轮完整的消息序列,包括系统提示词(截断版)、用户 query、模型中间推理、工具调用指令、工具返回结果。这样能直观看到模型“看到的”是什么,从而定位描述问题。后面排查部分会再展开。
3. 知识层实现:怎么给 Agent 搭一个“书架”
3.1 知识加载与切分:从文档到向量
知识层的目标不是把整本书塞给模型,而是把书拆成“词条”,模型需要什么就“翻到哪里”。这个过程分三步:加载、切分、向量化。
先说加载。AgentScope Java 支持从本地文件、URL、数据库等来源加载知识文档。文件格式常见的有 txt、pdf、markdown。pdf 是个大坑,因为格式五花八门,有的文字能被提取出来,有的是扫描图片需要 OCR。我的经验是:尽量让客户提供 markdown 或 txt 格式的源文件,实在只有 pdf 就先用第三方工具转一遍文本,再交给知识层。否则你的知识层处理时间会被 PDF 解析拖垮。
再说切分。文档切分的粒度直接决定了检索效果。切太大了,一个片段里塞的内容太多,向量化的语义会“糊掉”,检索时召回一堆不相关的片段;切太小了,上下文片段碎片化,模型拼不回完整的逻辑链。我常用的切分配置是按段落先粗切,再按固定窗口微调,比如 min_chunk_size = 200 字符,max_chunk_size = 800 字符,重叠区 overlap = 50 字符。重叠区是为了保证跨段落的语义在检索时不丢失——如果一句话被拦腰斩断,切分衔接处就会丢失核心信息。
最后是向量化。AgentScope Java 本身不内置 embedding 模型,它通过接入外部嵌入模型服务来生成向量。业界常做的方案是调用 OpenAI 的 text-embedding-3-small(如果你接受数据出网),或者本地跑一个 BGE 系列模型(通过 ONNX 或者 FastAPI 包装)。国内团队用的多的是部署本地开源 embedding 模型,因为私有化部署场景里数据不出域是硬性要求。
关于向量的存储和检索,AgentScope Java 提供了一套统一的检索接口,底层可以用向量数据库实现,常用的有 Chroma、Milvus、FAISS。本地开发我用 FAISS 偏多,因为它轻量、无服务端依赖。生产环境则建议用 Milvus 或云上的向量库,支持水平扩展和持久化。
3.2 检索召回:TopK 怎么选才能既准又不“撑爆”上下文
知识检索的经典流程是:把用户问题向量化,然后在向量库里做近邻检索,返回最相似的 K 个片段。这里有几个关键词值得讨论:相似度阈值、TopK 大小、重排(Rerank)。
相似度阈值决定了“宁缺毋滥”。如果用户问的内容和知识库里的文档完全不搭,检索出来一堆低相似度的片段,拼进上下文只会干扰模型判断。我实际用的阈值是 cosine 相似度 0.7(具体还要看你用的 embedding 模型的相似度分布)。低于这个阈值的片段一律过滤掉。注意这个阈值不是拍脑袋拍的,要根据你的知识库在真实 query 下的召回分布来调。方法很简单:准备 50 条真实用户问题,跑一遍检索,把召回的相似度打印出来,找到明显分层的那个阈值点。
TopK 大小要考虑模型的上下文窗口。假设模型上下文是 8K token,系统提示词 + 历史对话已经占了一半,那知识片段最多只能放 2K~3K token。按平均每个片段 150~250 个 token(中文大概是 200~300 字,具体取决于分词器),TopK 设在 5~8 比较稳妥。K 太大会挤占对话空间,K 太小又可能漏掉关键信息。一个工程建议:不要让知识片段“裸奔”,每个片段前面加一个元数据头,比如来源文档名、章节号、日期,模型看到这些能更好地判断片段的可信度和时效性。
**重排(Rerank)**是目前业界事实上的标配。向量检索是第一道粗筛,重排模型再对召回的片段和用户问题做精细语义相关性打分,把最相关的 few 个片段挪到最前面。原因是向量检索的语义匹配能力有限,有时候召回的前几名并不一定是最相关的。我在实测中加了一个轻量 Rerank 模型(比如 BGE-reranker)之后,回答准确率从 60% 左右提升到了 75% 以上。AgentScope Java 的相关设计里有没有内置 Rerank 我不确定(取决于你用的版本和扩展包),但即使没有,你也可以在检索后处理环节自己加一个 Rerank 服务接口,原理上完全可行。
// 知识检索的伪代码示意 KnowledgeQuery query = KnowledgeQuery.builder() .queryText(userQuestion) .topK(8) .similarityThreshold(0.7) .build(); List<KnowledgeFragment> fragments = knowledgeStore.search(query); // 可选:在 fragments 进入上下文前做后处理 List<KnowledgeFragment> reranked = rerankService.rerank(userQuestion, fragments);3.3 让 Agent 学会“查书架”:知识引用与来源说明
知识层的另一个重要设计是“引用溯源”。Agent 基于知识片段回答问题时,应该告诉用户“这是基于哪份文档说的”。这不仅是用户体验问题,还是工程调试的依据——当回答出错时,你能快速定位是检索出了问题、知识片段本身有问题,还是模型理解有偏差。
我的做法是在每个知识片段上捆绑一个 source 字段。比如:
public class KnowledgeFragment { private String content; // 片段正文 private String source; // 来源,如:doc://hr-policy/第三章/第2节 private double score; // 相似度得分 }在拼装系统提示词时,我要求模型在引用知识库内容时附加 source,格式如“根据《员工手册》第三章第 2 节”。为了强制模型遵守这个约定,我会在系统提示词里明确写一条规则:“当回答基于知识库内容时,必须在句末附带来源标识。若信息不在知识库中,必须明确回答‘知识库中未找到相关信息’,不得猜测。”实测下来,这种显式约束比放任模型自由发挥有效得多。
另外说一句,知识检索和工具调用不是割裂的。在很多实际场景里,它们是协作的。最常见的模式是:工具负责拿到“活数据”(比如实时库存、账户余额),知识库负责提供“静态规则”(比如库存流转制度、审批流程说明),模型综合两者给出回答。比如用户问“我能不能申请设备采购预付款”,知识库提供《财务报销制度》条款,工具查询用户的采购单状态,Agent 把两者结合回答“根据制度第 X 条,你符合条件,但当前采购单状态为待审批”。在 AgentScope Java 里,这种协作天然支持,因为工具结果和知识片段都会作为消息序列的一部分被模型同时看到。
4. 实操过程:在 AgentScope Java 里把工具和知识接起来
4.1 工程配置与依赖
我假设你已经有一个能跑通前面两篇基础对话的 AgentScope Java 工程。如果没有,赶紧回头把基础链路通一遍,不然后面每一步都会卡壳。接下来你需要添加的知识与工具层相关依赖,核心是 agent-tool 和 knowledge 相关的模块。不同版本包名会变,建议以你项目中实际引入的 SDK 版本号为基准,别直接照抄网上的旧代码。
一个常见的初始化片段是这样:
// 初始化工具注册中心 ToolRegistry toolRegistry = new ToolRegistry(); toolRegistry.register(new WeatherTool()); toolRegistry.register(new OrderQueryTool()); // 初始化知识库 KnowledgeStore knowledgeStore = new KnowledgeStore(vectorStore); knowledgeStore.load("hr-policy.md"); // 创建 Agent 并挂载两层能力 Agent agent = Agent.builder() .model(modelConfig) .tools(toolRegistry) .knowledge(knowledgeStore) .reActLoop(true) .build(); // 发起对话 AgentResponse response = agent.chat("北京明天是晴天吗?");这段代码看着简单,但配置上有几个“隐形开关”直接影响能否跑通:
开关一:reActLoop(或等价命名,如 enable_tool_call_loop)。默认情况下 Agent 可能只做单轮模型调用,不自动执行工具结果回填。如果你发现调用了工具但模型拿不到结果、或者模型压根不发起工具调用,先检查这个开关有没有打开。这是工具层失效的第一大原因。
开关二:system prompt 模板。AgentScope Java 里工具有效工作的前提是系统提示词里包含工具使用规则。有的版本会自动拼接,有的版本需要你在系统提示词模板里加占位符。你需要检查生成的系统提示词里是否能看到工具名和 JSON Schema。看不到,就是注册没成功,或者提示词模板没配置。
开关三:模型的能力开关。不是所有模型都支持 function calling。有些模型(尤其是本地部署的某些开源模型)能力较弱,需要额外在提示词里用 ReAct 文本格式引导,而不是用原生的 function calling 协议。AgentScope Java 是否能做降级处理,取决于版本。如果模型不支持 function call 格式,你会看到模型始终不输出工具调用指令,或者直接答非所问。
4.2 系统提示词的组装策略
工具和知识层能不能生效,一半靠代码,一半靠提示词。AgentScope Java 会把工具 JSON Schema 自动拼进系统提示词,但你最好在系统提示词里再加上一层“使用策略”,让模型知道在什么情况下优先用什么、以及各种边界规则。
我习惯在系统提示词里固定加这么几段内容:
工具使用规则: 1. 当问题涉及实时数据、用户私有数据、需要执行操作的场景时,必须先调用对应工具,不得凭已有知识臆测。 2. 工具调用失败时,如实告知用户调用失败的原因,不要伪装成功。 3. 多个工具可从不同维度提供信息时,可以连续调用多个工具后再统一回答。 知识库使用规则: 1. 回答与公司制度、产品说明、政策流程相关的问题时,必须优先引用知识库片段。 2. 引用时标注来源编号,格式: [来源:xxx] 3. 知识库片段无法覆盖用户问题时,明确说“知识库中未找到相关信息”,禁止编造。你可能会问:工具 JSON Schema 里不已经写了描述吗,为什么还要在系统提示词里重复规则?实测下来的原因是:JSON Schema 里的描述通常比较简洁,模型对“何时该用”的判断容易摇摆;而系统提示词里的规则部分用自然语言写得更详细,能显著提高模型遵守工具的稳定性。这两者是互相补充的关系,不是冗余关系。
4.3 从单工具到双能力:一个完整的端到端例子
为了让你更直观地看到全链路,我给一个完整的例子:用户问“小明上周提交的采购申请批了吗?”这个场景需要同时用到工具(查审批状态)和知识(查审批制度里“审批时限”条款)。
第一步,工具注册两个方法:
public class ApprovalTool { @AgentTool( name = "query_approval_status", description = "根据申请人姓名或申请单号查询审批状态。当用户询问审批进度、审批结果时必须调用。", parameters = { @ToolParam(name = "applicant", type = String.class, description = "申请人姓名", required = true) } ) public String queryApprovalStatus(String applicant) { // 模拟查询数据库 return "申请单号 AP20240115,状态:审批中,当前节点:财务复核,提交时间:2024-01-15"; } }第二步,知识库加载审批制度文档,检索“审批时限”相关片段。
第三步,Agent 的 ReAct 循环会这样走:
用户: 小明上周提交的采购申请批了吗? 模型: 我需要先查一下审批状态。调用工具 query_approval_status(applicant="小明")。 Agent执行工具,返回: 申请单号 AP20240115,状态:审批中,当前节点:财务复核。 模型再次推理: 审批还在进行中。我需要补充知识库中关于审批时限的信息,判断这个进度是否正常。 Agent检索知识库,返回: 《采购审批制度》第5条:一般采购审批时限为7个工作日,复杂采购审批时限为15个工作日。 模型生成最终回答: 小明的采购申请(AP20240115)还在审批中,当前在财务复核节点。根据《采购审批制度》第5条,一般采购审批时限为7个工作日,目前还在正常时限内,建议继续等待。这个过程看起来顺理成章,但请注意:模型做“先查工具、再查知识库”的决策不是天然就会的。在第一次运行时你极可能遇到模型只调工具、不查知识库,或者干脆跳过工具凭印象直接回答。这时候你就要对系统提示词下功夫,明确引导“涉及进度查询先调工具,涉及制度规定再查知识库”。这属于 Agent 工程里最常见的“模型行为调教”环节。
4.4 检索质量与回答质量的验证方法
工具和知识层接上后,最大的问题就是怎么验证效果。聊得热闹不如可量化。我给自己定的一套验证 checklist 是这样的:
召回验证:用 20~30 条真实问题做测试集,逐条跑知识检索,人工判断召回的前 5 个片段里有没有正确答案。如果召回率低于 70%,说明切分粒度、embedding 模型或向量库索引需要调整。
工具调用准确率:测试模型发起工具调用的时机和参数是否正确。统计“应该调用时没调用”和“不该调用时乱调用”两类错误,分析错误原因是描述不清晰还是规则缺失。
端到端回答质量:把问答对交给业务方打分,重点看事实准确性、来源标注合规性、表述是否自然。这一步往往能发现知识库里真正缺了哪块内容。
我特别建议记录失败案例而不是只统计准确率。每一条“模型没调用工具”“检索回了错误片段”“回答与知识库不符”的案例,都是调整提示词、切分参数、阈值的直接依据。做过几轮迭代后,你会发现准确率的提升瓶颈往往不在模型,而在知识库质量和工具描述质量。这个判断和很多人的直觉相反,但确实是工程现实。
5. 常见问题与排查技巧实录
5.1 错误排查的“总思路”:先看消息序列,再谈代码
Agent 出问题时,我最反感的是直接去改代码。Agent 的行为是由上下文的“输入”决定的,代码只负责把上下文组织好。所以排查的第一步永远是打印完整的消息序列,看模型在这一轮看到了哪些消息,然后推测问题的根源在哪个环节。
我在项目里加的调试日志模板如下:
[MESSAGE EXCHANGE LOG] System Prompt (前1000字符): ... User Query: ... Assistant Reasoning: "我需要查询该用户的订单状态" Tool Call Request: query_order(applicant="张三") Tool Response: {orderId: "123", status: "已发货"} Final Answer: "您的订单已发货"拿到这个日志后,问题定位基本可以按下面的顺序问下去:
- 模型有没有发起工具调用?没有 → 检查工具是否注册成功 / 系统提示词里是否有工具描述 / 模型是否支持 function calling。
- 工具调用参数对不对?不对 → 检查参数描述清晰度 / 类型映射配置。
- 工具执行有没有报错?有 → 看堆栈,重点检查反射调用和类型转换。
- 工具结果有没有正确回填?没有 → 检查 reActLoop 开关 / 消息 ID 关联逻辑。
- 最终回答有没有用到工具结果?没用 → 提示词里缺少“必须基于工具结果回答”的规则。
这套排查顺序能覆盖 90% 的工具层问题。知识层的问题排查类似:用户反馈回答不对 → 看检索召回的是什么片段 → 检查切分和向量化质量 → 检查 TopK 和阈值 → 检查提示词是否要求引用知识库。
5.2 工具层高频问题速查表
我整理了一张表,都是我在实际项目里真实遇到过的坑,每条后面都跟了解决思路。
| 问题现象 | 根因分析 | 解决方案 |
|---|---|---|
| 模型完全不调用工具,把工具名当普通文本回复 | 1. 模型不支持 function calling 协议 2. 工具描述在提示词里不可见 | 1. 换支持 function calling 的模型 2. 检查提示词拼接,确认 JSON Schema 已注入 |
| 工具被调用但参数是乱码 | 字符编码不一致,中文参数在 JSON 转换中被破坏 | 统一配置 UTF-8 编码,检查框架默认 charset 设置 |
| 工具调用报类型转换异常 | Schema 中参数类型与实际入参类型不匹配 | 参数描述里写明格式样例,或改用 String 接收 + 内部解析 |
| 工具返回值太长,模型上下文被撑爆 | 工具返回了大段明细数据 | 工具层做摘要后返回,只回传必要字段 |
| 工具调用成功但模型不用结果 | 系统提示词未强制要求基于工具结果回答 | 提示词中显式声明:回答必须基于最近一次工具返回的数据 |
| 多个相似工具,模型频繁选错 | 工具描述边界模糊 | 给每个工具补全差异化触发场景描述,必要时用工具分组 |
| 工具方法里有状态改变(如写库),模型重复调用 | 模型在推理中重复生成同一工具调用 | 工具层增加幂等性设计:调用前检查是否已执行过同一请求 |
这里面的“模型重复调用“是特别隐蔽的一个问题。有一次我们给 Agent 挂了“发送短信”工具,结果模型在输出最终回答之前连续生成了两次调用指令,导致用户收到两条短信。排查后发现是模型对工具结果的处理策略不明确——它不确定工具调用已经完成,于是又试了一次。解决办法是在工具返回消息里增加一个状态字段,明确标记“短信已发送成功,无需重复调用”,并在系统提示词里写“工具调用成功后不要再次调用相同工具”。
5.3 知识层高频问题速查表
知识检索是“看起来简单、调起来磨人”的模块。我把常见问题也整理成表:
| 问题现象 | 根因分析 | 解决方案 |
|---|---|---|
| 检索结果和问题完全无关 | 1. 切分粒度太粗或太细 2. embedding 模型与语料领域不匹配 | 调整切分参数;换用领域相关的 embedding 模型,或用真实问题验证召回 |
| 召回片段正确但模型没用 | 知识片段被拼接在上下文末尾,模型没读到 | 调整知识片段在提示词中的位置,或减短历史对话长度 |
| 相似度高但答案错误 | 知识片段本身不完整,或者多片段拼接造成歧义 | 给知识片段增加上下文补充窗口(前后扩展一段),让模型读到完整语义 |
| 中文问答效果差 | 部分 embedding 模型对中文支持弱 | 换用中文优化过的模型(如 BGE 系),或者微调切分策略适配中文分词 |
| 文档更新后检索结果仍是旧内容 | 向量缓存未失效 | 知识库更新时同步删除旧 chunk 的向量,重建索引 |
| 多个相似文档片段互相矛盾 | 知识库中存在冲突内容,模型难以取舍 | 在元数据里增加版本号/发布日期,提示词中要求优先选择“最新版本” |
5.4 关于系统提示词的两点独家心得
最后补两个我踩过几轮才悟出来的技巧,它们不属于 AgentScope Java 的 API 范畴,但对工具层和知识层能否正常工作影响巨大。
技巧一:给模型一个“别乱动”的兜底选项。在系统提示词里加上“如果工具结果和知识库内容与问题无关或冲突,直接说明无法回答,不要强行调用工具”。这条规则能让模型的调用行为更克制,减少误调用和幻觉。我把它称为“刹车规则”。没有这条规则时,模型会倾向于“尽量调点什么”,反而把简单问题搞复杂。
技巧二:用“链式思考示例”在前面加一个 few-shot 样例。单纯写规则,模型可能还是不知道具体怎么把工具和知识串起来。我在系统提示词里加了一个简短的 few-shot 示例,模拟用户问一句、模型内部推理、调用工具、查知识库、最终回答的完整过程。模型看到这种示例后,复现的稳定性会明显提升。这是低成本但高回报的调教手段。
Agent 开发有个特点:你写下的每一行代码,都是在为模型的决策做“场景铺垫”。工具和知识层不是简单的接口封装,它们定义了 Agent 能力的边界。手上有工具,书架上有知识,Agent 就不再是只会复述训练数据的聊天机器人,而是一个能查数据、能执行操作、能依据内部文档做判断的“数字员工”。
如果你照着前面的步骤把工具和知识层接上了,我建议你做的第一件事不是继续加功能,而是拿一套真实业务问题去跑一遍回归测试,把每一轮的回答和消息日志存下来。这些数据会告诉你,你的 Agent 离“可信可用”还差多少调整。下一轮迭代你会感谢今天埋下的这个测试基础。