用 ToolExecutionContext,让大模型少猜一个 userId

假设我们要做一个很简单的功能:用户说“帮我查一下订单 A001”,大模型调用工具,返回订单状态。

工具查询订单需要两个信息:订单号,以及当前用户是谁。订单号可以让模型从这句话里面提取,但 userId 也要交给模型填写吗?

如果把两个字段都放进工具参数,模型就得生成类似这样的内容:

{"order_id": "A001", "user_id": "user-001"}

问题是,userId 可能填错。模型可能照抄提示词里的示例,也可能拿到聊天记录里出现的另一个 ID。提示词写一句“不要传错”,并不能让这个参数变得可靠。

既然后端已经知道当前用户是谁,我们直接把身份传给工具就行了。下面用 AgentScope Java 1.x 做一个小例子。

先创建一个保存身份的对象

我们自己定义一个 UserContext,它的职责就是保存 userId:

public record UserContext(String userId) {}

为了把例子讲简单,这里假设服务端已经确认当前用户是 user-001,直接创建对象:

UserContext userCtx = new UserContext("user-001");

这个固定值只是演示。实际使用时,应该替换成服务端认证得到的用户 ID,不能从聊天内容里面取。

再写一个查询工具

这里不用数据库,就准备两条模拟订单:A001 属于 user-001,B001 属于 user-002。

public class OrderTools {
    private final Map<String, String> owners = Map.of(
            "A001", "user-001",
            "B001", "user-002"
    );

    @Tool(name = "query_order", description = "查询当前用户的订单状态")
    public String queryOrder(
            UserContext userCtx,
            @ToolParam(name = "order_id") String orderId) {

        if (userCtx == null) {
            return "缺少用户身份,无法查询";
        }
        if (!userCtx.userId().equals(owners.get(orderId))) {
            return "订单不存在或无权查看";
        }
        return "订单已发货";
    }
}

注意这里两个入参的区别:orderId 带了 @ToolParam,需要模型填写;userCtx 没带这个注解,是我们准备让框架注入的上下文对象。

工具拿到对象以后,通过 userCtx.userId() 取得身份,再判断这个订单是不是当前用户的。UserContext 保存信息,OrderTools 使用信息,职责就这么简单。

把这个对象注册给 Agent

接下来,我们把工具注册到 Toolkit,再把 userCtx 注册到 ToolExecutionContext。假设 model 已经配置好:

Toolkit toolkit = new Toolkit();
toolkit.registration().tool(new OrderTools()).apply();

ToolExecutionContext toolCtx = ToolExecutionContext.builder()
        .register(userCtx)
        .build();

ReActAgent agent = ReActAgent.builder()
        .name("order-assistant")
        .model(model)
        .toolkit(toolkit)
        .toolExecutionContext(toolCtx)
        .build();

register 的作用,就是把我们创建的 UserContext 交给框架。等调用工具时,框架可以根据参数类型找到这个对象。

调用的时候发生了什么

框架生成工具 schema 时,不会把这里的 userCtx 放进去。模型看到的只有 order_id,所以用户问“查一下 A001”,模型只需要生成:

{"order_id": "A001"}

真正执行 queryOrder 时,框架从 JSON 里面取出 A001,再从 ToolExecutionContext 里面取出 UserContext,最后一起传给工具。

我们创建的 UserContext → 工具的 userCtx
模型生成的 order_id   → 工具的 orderId

这样,模型就不负责填写 userId 了。即使它额外生成一个 user_id 字段,也不会替换我们注册的上下文对象。

模型传 A001,工具返回“订单已发货”;如果传 B001,虽然订单确实存在,但它属于另一个用户,工具会拒绝返回状态。

这里防住的是身份参数被模型填错,订单号仍然需要检查归属。另外,不出现在工具参数里,也不代表 userId 在整个对话中绝对不可见。关键是执行时采用我们提供的身份。

后端已经知道的值,就由后端传给工具。我觉得没必要让模型再猜一次。

参考:AgentScope Java v1 工具执行上下文文档。示例采用 1.x API,省略 import 和模型配置。

bgg1s