Agent要提交订单,能让它直接执行吗
比特严选的用户发来一句话:
想换个手机,预算七千以内,平时主要拍照、刷视频,不怎么玩游戏,帮我挑一下。
Agent可以先查询在售商品,按预算和用途解释候选机型的差别。影响选择的信息不够,就再问一句。

用户看过推荐,选定iPhone 17,接着说:
就这款,256G黑色,买一台,先放购物车。
走到这里,Agent要做的事变了:前面只是查询和建议,这一步要真的改动用户的购物车。用户已经说清买哪款、买几台,Agent把这些要求填成工具参数以后,能不能马上执行?

本篇就从这里开始,跟着用户走到提交订单,看看Ragent怎样在执行之前让用户核对,以及用户同意或取消之后,程序怎样接着处理。
下文对话是讲解示例,商品配置来自项目初始化比特严选数据。源码按AgentScope Java 2.0.2解读。
从帮用户挑手机,到替用户执行操作
先把这次加购说具体。
同一款手机,容量和颜色不同,在商城里就是不同的SKU,每种能下单的配置各有一个商品编码。用户选的iPhone 17 256GB黑色,编码是MG6W4CH/A,Agent从前面的商品查询结果里拿到它。
Agent据此准备调用购物车工具update_cart_item:操作action填add,商品编码填MG6W4CH/A,数量填1。
参数填好以后,Ragent不会马上执行,而是先在页面上弹出一张确认卡,列出这次准备提交的操作、商品编码和数量,用户点同意才执行。
用户明明已经说了要加购,为什么还要再确认一次?因为用户说的是一句话,工具收到的是模型转换出来的参数:商品编码选的是不是那一款、数量是不是1,都可能和用户的原意有出入。程序能做到的是执行前停下来等着;参数对不对,要用户自己在卡片上看。
加购之后,用户还会要求下单。比特严选的下单工具create_order只能从购物车里挑商品建单,所以这个场景是先加购、再下单。
订单创建成功后,即使用户还没付款,系统也已经占用了库存,并从购物车扣除了本次购买的数量。
为什么在工具执行前确认
沿着购买过程,用户可以在三个时机介入,每个时机能核对的东西不一样:
| 介入时机 | 用户此时能核对什么 | 本次下单是否已经改变业务数据 |
|---|---|---|
| 开始选购时 | 预算、用途、愿意交给Agent办哪些事,具体商品可能还没选定 | 没有 |
| 下单参数准备好、工具执行前 | 即将提交的商品、数量和收货信息 | 没有 |
| 工具执行结束后 | 已创建的订单及执行结果 | 成功时已经改变 |
如果开始时授权,用户批准的是一个笼统的任务,那时买哪台、寄到哪还没定。执行后再告知,用户看到的已经是建好的订单,发现地址不对,只能去改单或者取消订单。
Ragent对需要确认的工具选了中间这一种:参数准备好后先暂停,等用户核对完再执行。也有产品在用户事先明确授权的范围内直接执行,这同样可行;Ragent选的是每次都让用户核对。
在提示词里加一句“下单前请询问用户”,也能引导模型先问。但问不问、问的时候说的和最后真正提交的参数是不是一回事,都取决于模型这一次怎么输出,比如它输出上说买1台,调用时却填了2。所以Ragent把确认检查放在工具执行之前,交给代码判断:只要这个工具配置成需要确认,不管模型问没问,都会先停下来等用户点。
读源码之前,先分清一次下单会经过谁:
- 模型:决定调用哪个工具、参数填什么。
- AgentScope:负责“模型推理 → 执行工具 → 结果交回模型”这个循环,也提供暂停等用户确认的能力。
- Ragent:告诉框架哪些工具要确认,把框架发出的事件转成页面上的确认卡,并保存会话状态。
- MCP Server:真正改购物车、建订单的业务代码在这里运行。
MCP Server上的每个工具,在Ragent里都由一个McpToolProxy代理,框架调工具实际调的是这个代理。执行之前,框架会先调用代理的checkPermissions(),问这次调用能不能直接执行:
public Mono<PermissionDecision> checkPermissions(Map<String, Object> toolInput, PermissionContextState context) {
// 把项目的确认要求交给框架,由框架决定后续是否执行
return Mono.just(needsConfirm
? PermissionDecision.ask("该操作执行前需要你确认")
: PermissionDecision.allow("该工具无需执行前确认"));
}
ask表示需要用户确认,allow表示通过这一项检查。这个方法只返回判断,没有创建确认卡,也没有去改购物车。接下来怎样暂停,要看AgentScope对这个结果的处理。
跟着一次下单走完确认流程
用户确认加购,工具返回成功后,用户继续说:
就把刚才选的这台下单吧,购物车里其他的先不买,也先别付款。
准备下单时,Agent先调query_cart查看购物车,核对待买的商品、数量和价格。这次查询只是帮Agent填参数,购物车和库存够不够,下单工具执行时还会再查一遍。

用户只要刚才那台手机,购物车里的其他商品先不买,所以购买清单里只有这一项,数量是用户说的1台。
收货人、手机号和地址这三项,不填时下单工具会沿用该用户最近一笔订单的收货信息。本例假定用户在对话里把这三项都补齐了,也不用优惠券。
下单工具用items表示购买清单。本例填入[{"skuCode":"MG6W4CH/A","quantity":1}],意思是只买这台手机1台。没有列出的购物车商品不进入本单。
比特严选里,update_cart_item和create_order都配置成了需要确认,所以这段购买过程会先后弹出两张确认卡,一张加购,一张下单。两张卡走的是同一套流程,下面只拆下单这一张。
图里画的是两次请求:第一次,用户发出下单请求,页面弹出确认卡;第二次,用户点同意,后端接着执行。之前选手机、加购、补地址的那几轮对话没有画进去。
1. 模型填好参数,框架记录调用并等待确认
模型输出create_order和参数,只表示它提出了这次调用,业务接口此时还没有执行。
AgentScope用ToolUseBlock记录一次工具调用,里面有调用ID、工具名、参数和调用状态。同一个工具在一段对话里可能被调用很多次,光凭create_order这个名字分不清是哪一次,所以每次调用都有自己的ID。后面用户点同意,批准的就是这个ID对应的那一次。
checkPermissions()返回ask后,框架把这次调用的状态标成ASKING,意思是等用户选择。这是工具调用的状态,不是订单状态,此时订单还没有创建。
ReActAgent.actingStream()中决定是否进入工具执行的分支如下;这里的pending是需要用户确认的调用列表。
if (pending.isEmpty()) {
// 没有等待用户确认的调用,才进入本批工具执行
return runToolBatch(toolCalls, autoDenied, replyId, resultHolder);
}
// 省略自动拒绝结果的记录与结果容器初始化
persistPendingConfirmRequest(replyId);
// 通知接入方需要确认,并要求结束本次运行
return Flux.<AgentEvent>just(
new RequireUserConfirmEvent(replyId, pending),
new RequestStopEvent(
"permission asking", GenerateReason.PERMISSION_ASKING));
有待确认的调用时,框架连发两个事件:RequireUserConfirmEvent带着待确认的调用,交给Ragent去做确认卡;RequestStopEvent让本次运行停下,停止原因记为PERMISSION_ASKING,也就是在等用户确认。
待确认的调用记在模型刚输出的那条助手消息里,状态是ASKING。这条消息和前面的对话一起,组成框架的上下文,也就是会话状态里的对话记录。persistPendingConfirmRequest()再在这条消息上记下本次回复的ID(replyId),用户点同意时发来的下一次请求,就能据此对上这次确认。
到这一步,改动还只在内存里。本次运行正常结束时,doCall()把整个会话状态交给存储,Ragent配的存储把它写进PostgreSQL。
所以这里的暂停,不是让一个线程挂着等用户点击,而是把能恢复的状态存进数据库,结束这次运行,等用户点击时由一个新请求接着跑。
从上面的分支还能看出,只要pending不为空,runToolBatch()就不会执行。所以模型同一批提出多个调用时,只要其中一个要确认,同批的其他调用也要一起等。