Skip to main content

加载技能前后,模型能看到哪些工具

上一篇那句“那台iPhone 18 Pro Max我不想要了,还没发货吧?帮我取消”,模型先加载订单变更手册,再查订单,第三轮才发起cancel_order,停在确认卡。这次关心的不是它调了什么,而是每一轮它能调什么。把这个会话三轮推理前送给模型的工具清单摆出来:

推理轮次此时上下文里有什么模型拿到的工具其中有没有cancel_order
第1轮只有用户那句话11个没有
第2轮多了load_skill的结果,正文是订单变更手册13个有
第3轮又多了query_order的结果,五笔订单13个有

第1轮到第2轮多出来的两个,是cancel_order和change_address,正好是订单变更手册声明文件里tool-ids的那两个,也就是库里t_agent_skill.tool_ids那一列。模型第一轮看不见取消订单这个工具,加载手册之后才看见。

这件事和第一章讲MCP工具时说的对不上。那篇讲过,AgentToolCatalog.buildToolkit()把search_knowledge、load_skill、记忆整理工具flush_memory和意图树上14个MCP工具全注册进了同一个Toolkit,一共17个。既然cancel_order一直注册着,为什么第一轮模型拿到的清单里没有它?

因为注册了和这一轮给模型看了是两回事。注册进Toolkit的是17个能执行的工具对象,Agent实例建好之后这份就固定了,谁来问都是这17个。模型每轮看到的不是它,而是框架在推理前从这17个里整理出来的一份工具schema列表,生命周期仅当前这轮,下一轮再整理一份。Ragent在整理的时候自定义了一手:手册没加载的,它解锁的那几个工具就不放进这一轮的列表。

上一篇只定义到“tool_ids是加载后才解锁的工具”,为什么要藏、每轮怎么算、手册没了怎么收回,是本篇的事。

为什么要隐藏,藏哪些​

1. 看得见工具不等于知道规则​

上一篇讲过,取消订单的规则不在工具描述里:先查订单拿状态、四个状态四条路、取消前把退券退款回补讲给用户。它们写在手册里,手册加载了模型才知道。

如果cancel_order一直在清单里,模型完全可以跳过手册直接调它。用户一句“帮我取消88239”,模型有订单号、有工具,没有任何东西要求它先去取手册。第一章定的两道代码关卡这时都帮不上:确认卡只管提交前要用户点头,服务端只管这单现在能不能取消,谁都不看模型有没有先把退款退券讲清楚。

把工具藏起来,是让模型在工具清单里找不到“直接办”这条路。它要取消订单,清单里能对上的只有load_skill,而load_skill的描述头第三行写着“部分业务工具在关联手册加载后才会出现在可用工具中,不要仅凭当前工具清单判断是否支持该任务”,人设里对应的一句是“处理用的工具往往加载手册后才出现,判断入口是技能清单不是工具清单”。机制和文字配在一起:清单里没有,模型就去找手册;找到手册,工具才出现。

藏起来的只是这一轮送给模型的清单。工具名还会出现在别的地方:手册正文里写着它,前几轮调过它的消息也还留在上下文里。这里延伸出一个问题:手册被压缩带走之后这些名字还在,模型顺着前文再调一次怎么办?后面讲收回时一起说。

2. tool_ids只放不看手册会办错的那几个​

五份手册的tool_ids,按库里现在的配置:

技能标识tool_ids这些工具的声明
after_sale_applyapply_after_sale写
order_changecancel_order、change_address写
fault_diagnosis空
order_checkoutupdate_cart_item、create_order、pay_order写
iot_bundle空

六个Tool全是写操作,但判据不是“写操作就挂”。submit_ticket也是写操作,也勾了执行前确认,它不在任何手册的tool_ids里,因为提交工单不需要先走流程。反过来,query_order是订单变更手册第1步就要用的工具,它也不挂。退换货手册的声明文件把理由写在注释里:

# 只挂 apply_after_sale:query_order、query_after_sale 看自己的参数说明就能调对,
# 用户单独问「我那张售后单到哪一步了」时不该先加载一份办事手册
skill-code=after_sale_apply
tool-ids=apply_after_sale

挂进tool_ids的代价是:用户只想查一下售后进度,模型也得先加载一份办事手册才看得见查询工具,多转一圈。所以只挂那些“看自己的参数说明调不对、必须先看手册”的工具。选购下单手册同理,query_cart、query_coupons、search_product都不挂。

由此得出两条规则。一是不在任何tool_ids里的工具始终可见,加不加载手册都一样。二是正文引用不等于需要解锁。订单变更手册正文写了query_order,但query_order不在它的tool_ids里,加载前照样能调;AgentSkill记录上toolIds字段的注释原话是“加载本技能后才解锁的MCP工具ID,未加载时对模型不可见;正文按名引用其它工具不受影响”。

管理端保存手册时,AgentSkillAdminServiceImpl.validateToolIds()只认意图树上已启用的MCP节点,所以search_knowledge、load_skill这类进程内工具挂不进tool_ids,遮蔽只会落在MCP工具上。

每轮推理前算一次遮蔽​

1. 遮蔽 = 全部解锁工具 − 已加载手册的解锁工具​

拿开头那个会话算一遍。第一轮推理时,消息列表里只有第1条用户消息,没有任何TOOL消息,所以“已加载”集合是空的。五份手册里三份带tool_ids,并起来六个工具。已加载为空,六个全在遮蔽集合里,17个注册工具减掉6个,模型看到11个。

模型调了load_skill(order_change),结果作为第3条TOOL消息进了列表,metadata里带着"ragent_loaded_skill": "order_change"。第二轮推理前再算:已加载集合变成{order_change},它的两个工具cancel_order、change_address从遮蔽集合里减掉,剩四个继续藏,模型看到13个。第三轮query_order的结果进来,没有新的加载标记,遮蔽集合不变。模型在这一轮发起cancel_order,这时它在清单里。

开头那张表只写了模型每轮拿到11个、13个工具,没说少掉的是哪几个。下面把六个解锁工具逐个摊开,看每一轮它是被藏着还是放出来了。藏与放只看一件事:它所属的手册这一轮算不算已加载。

解锁工具所属手册第1轮(没加载任何手册)第2轮(已加载order_change)第3轮(已加载order_change)
cancel_orderorder_change藏放出放出
change_addressorder_change藏放出放出
apply_after_saleafter_sale_apply藏藏藏
update_cart_itemorder_checkout藏藏藏
create_orderorder_checkout藏藏藏
pay_orderorder_checkout藏藏藏

第1轮六个全藏,17个注册工具里模型拿到11个;第2轮起订单变更手册的两个放出,模型拿到13个。另外11个不在任何手册tool_ids里的工具,三轮都在清单里,不用列。这张表是按开头说的办法复算出来的:喂的是这个会话落库的消息列表,算的是实际代码运行中的记录。

把上面那张表算出来的是AgentSkillMaskingMiddleware.maskedTools()。它的产物是一个要藏的工具名集合,下文叫遮蔽集合:第1轮是那六个,第2轮是四个。算法就是小节标题那个减法,两步:先把五份手册tool_ids里的工具全部收进集合,再把已加载手册的tool_ids从里面减掉。

// AgentSkillMaskingMiddleware.maskedTools(),返回这一轮要藏的工具名集合
private Set<String> maskedTools(List<Msg> messages) {
List<AgentSkill> skills = skillRegistry.listEnabled();
if (skills.isEmpty()) {
return Set.of();
}
// 从消息列表里读出这个会话此刻加载着哪几份手册,第 1 轮是空集,第 2 轮起是 {order_change}
Set<String> loaded = loadedSkillCodes(messages);
// 第一步:五份手册 tool_ids 里的工具全部收进来,就是那六个
Set<String> masked = skills.stream()
.flatMap(skill -> skill.toolIds().stream())
.collect(Collectors.toCollection(LinkedHashSet::new));
// 第二步:已加载手册的 tool_ids 从集合里减掉,第 2 轮减掉的是 cancel_order 和 change_address
skills.stream()
.filter(skill -> loaded.contains(skill.skillCode()))
.forEach(skill -> skill.toolIds().forEach(masked::remove));
return masked;
}

第1轮没有已加载的手册,第二步什么都不减,集合里是六个。第2轮已加载{order_change},减掉它的两个,剩四个。

一个工具可以挂在多份手册的tool_ids里。这时只要其中一份手册加载了,第二步就会把这个工具从集合里减掉,模型就看得见它,不用等其余手册也加载。比特严选的五份手册没有共用的工具,这条规则目前用不上。

解锁付费内容,👉戳​