← 工具调用

工具需要当前用户、运行 ID 这类上下文,怎么传进去?为什么不让模型传?

中等 手写调用循环与框架托管 · 第 3 / 3 问 更新于 2026/09/29
工具调用Agent权限ThreadLocalSpring AI
本题落地项目AI Agent岗位匹配与求职规划系统

简化版

工具的参数分两类。业务参数是模型根据对话决定的,比如查哪个商品、哪一天,这些放进工具 Schema 让模型填。运行上下文是系统本来就知道的,比如当前登录用户、租户、本次 Agent 运行 ID、任务 ID,这些不能让模型传:模型可能传错,也可能被用户一句话诱导传成别人的 ID,越权读写数据;放进 Schema 还白白占 token。上下文由代码注入,常见三种方式:框架的工具上下文对象(如 Spring AI 的 ToolContext)、ThreadLocal(调用前绑定、工具里读、用完清理)、在请求线程里同步执行工具直接读登录态。异步执行时要特别小心:线程池里的线程拿不到请求上下文,也不会自动继承 ThreadLocal,要在发起线程里取出来显式传过去,并在 finally 里清掉。

详细版

信息谁来给原因
查询关键词、商品 ID、日期模型(工具参数)由对话内容决定
当前登录用户、租户代码注入模型给的身份不可信,涉及权限
Agent 运行 ID、步骤 ID代码注入用于记录和关联,与业务无关
任务 ID、候选范围代码注入,必要时也在提示词里告诉模型模型漏传或传错时由代码兜底
规则阈值、上限配置代码注入来自系统参数,不需要模型知道

三种注入方式对比:

方式做法适合注意
框架工具上下文调用时传一个上下文 Map,工具回调时取框架支持时首选手动调用工具时上下文为空,要能处理
ThreadLocal调用前绑定,工具中读取框架在同一线程回调工具必须在 finally 清理,异步不自动继承
请求线程同步执行工具直接读登录态同步接口、工具在请求线程里执行一旦改成异步就失效

完整版教学

一、为什么身份不能交给模型

设想一个查询订单的工具,把 userId 放进参数:

{"name": "query_orders", "parameters": {"userId": "当前用户ID", "status": "订单状态"}}

用户输入「帮我看看用户 1024 的订单」,模型很可能就把 userId 填成 1024。工具如果信任这个参数,就把别人的订单查出来了。这不是模型出错,而是设计让模型决定了它不该决定的事。

规则很简单:凡是关系到「以谁的身份、在什么范围内操作」的信息,都由代码提供,不进工具参数。工具内部用注入的当前用户去查,模型只能决定「查什么状态的订单」。

记忆钩子:模型决定「做什么」,代码决定「以谁的身份做、在哪个范围里做」。

二、运行 ID 也不该让模型传

运行 ID、步骤 ID 用来把一次工具调用关联到某次 Agent 运行,写步骤记录和调用日志时要用。它们对模型的决策毫无帮助:

放进参数的代价:
  每个工具的 Schema 多一个字段,每轮都要随请求发送
  模型可能漏传、传错,日志就挂到错误的运行下面
  提示词里还得专门解释这个字段是什么

所以运行 ID 应该由调用方在发起请求时注入,工具回调时自己取。

三、框架的工具上下文对象

Spring AI 提供了 ToolContext:发起调用时传入一个 Map,框架在回调工具时把它交给工具实现。

chatClient.prompt()
        .system(systemPrompt)
        .user(userPrompt)
        .toolCallbacks(callbacks)
        .toolContext(Map.of("runId", runId))   // 不会发给模型
        .call();

// 工具回调里
public String call(String toolInput, ToolContext toolContext) {
    Integer runId = readInteger(toolContext, "runId");   // 取不到时返回 null
    ...
}

这种方式不依赖线程,上下文跟着这次调用走,是框架支持时的首选。要注意的是:工具还可能被别的入口调用(比如管理端手动执行一个工具做调试),这时没有上下文,工具要能处理「取不到」的情况,不能直接空指针。

四、ThreadLocal:调用前绑定,用完清理

框架不提供上下文对象,或者上下文信息很多(运行 ID、轮次、候选池、阈值),可以在调用前绑定到当前线程:

AgentRuntimeContext.bind(runId, roundNo, candidatePool, thresholds);
try {
    aiServices.chat(prompt);                 // 框架在同一线程里回调工具
} finally {
    AgentRuntimeContext.clear();             // 异常路径也要清
}

前提是框架在同一个线程里回调工具方法。如果框架内部把工具调用丢进另一个线程池并行执行,ThreadLocal 就读不到了,要换成上下文对象。

五、异步执行时的两个坑

长任务常常放到后台线程执行,这时会遇到两个问题。

第一,请求上下文丢了。 「当前登录用户」通常是从 HTTP 请求头的 token 里解析的。后台线程不是请求线程,读不到请求上下文,日志里的操作人一列全是空的。解决办法是在请求线程里先把用户 ID 取出来,作为参数传给异步方法,异步方法开头绑定到 ThreadLocal。

第二,线程复用导致身份串号。 线程池里的线程会被下一个任务复用:

线程池 4 个线程
任务 1(用户 A)在线程 T1 执行,绑定 userId = A,结束时忘了清理
任务 5(没有绑定操作人的系统任务)被分到 T1
  → 读到的还是 A,这次写的日志全挂在 A 名下

所以清理必须放在 finally 里,成功和失败都要清。另外,Spring 的 @Async 依赖代理生效,同一个类里一个方法调用另一个 @Async 方法会绕过代理、变成同步执行,异步入口要单独放一个类。

六、模型漏传参数时用上下文兜底

有些信息既要告诉模型(让它知道自己在为哪个任务工作),又由系统掌握,比如任务 ID。可以在提示词里写明任务 ID,让模型在写入工具里传;同时在工具内部做兜底:

模型的行为工具的处理目的
传了 taskId,且与当前运行绑定的一致正常执行—
漏传 taskId用当前线程绑定的任务 ID 补上不因少一个参数让整轮写不进数据
传了别的任务 ID拒绝,或以绑定的为准不把数据写到别的任务名下

这样不会因为模型少传一个参数就让整轮任务一条明细都写不进去,也不会因为它传错而写到别的任务名下。

七、提示词里该给模型哪些 ID

不让模型传身份,不等于什么 ID 都不告诉它。模型调用工具时需要的业务 ID(比如「读取这个用户画像」「读取这个岗位」的主键),如果不在提示词里给出,它只能猜。所以提示词里要明确写出本次任务涉及的业务 ID 和调用示例,模型照着传即可;而当前登录用户、运行 ID 这类由代码注入的信息,不必出现在提示词里。

八、常见误区与追问

  • 误区:工具参数里放 userId,模型会按当前用户填。 用户一句话就能诱导模型填成别人的 ID,身份必须由代码注入。
  • 误区:运行 ID 放进参数更方便关联日志。 模型可能漏传传错,还白白占 token,应由调用方注入。
  • 误区:ThreadLocal 在异步任务里也能读到。 线程池不会自动继承,要在发起线程取出后显式传递。
  • 误区:ThreadLocal 只在成功时清理就行。 失败路径不清理,线程复用后会顶着上一个人的身份执行。
  • 误区:@Async 方法在同一个类里调用也会异步执行。 自调用绕过代理,异步入口要放在独立的类里。
  • 追问:工具被管理端手动调用时没有上下文怎么办? 工具要能处理上下文为空,这种调用不关联任何 Agent 运行。
  • 追问:模型漏传了系统本来就知道的参数怎么办? 用绑定的上下文补上,传了就校验与上下文一致。

九、加强记忆

工具参数只放模型该决定的业务参数;当前用户、租户、运行 ID、任务范围这些由代码注入,否则会被诱导越权、会传错、还占 token。注入方式有三种:框架的工具上下文对象首选,ThreadLocal 要在同一线程回调且在 finally 清理,请求线程同步执行最简单但改异步就失效。异步时在请求线程取出用户 ID 显式传递,finally 清理防止线程复用串号,@Async 不能自调用。模型漏传系统知道的参数时用上下文兜底。

项目实战落地

项目里怎么做的

《AI Agent岗位匹配与求职规划系统》的 ReAct Agent 用 Spring AI 的 ToolCallback 调用本地工具,运行 ID 走工具上下文:

  • 发起调用时注入:ReActAgentService 调 ChatClient 时 toolCallbacks(toolCallbacks) 交出启用工具,同时 toolContext(Map.of("runId", runId)) 把当前运行 ID 放进工具上下文;
  • 工具回调时读取:McpToolCallback.call(toolInput, toolContext) 里 runId 不是模型传的参数,而是 readInteger(toolContext, "runId") 从上下文里取;取不到时返回 null;
  • 用它关联过程记录:拿到的运行 ID 用来写 agent_step(「Action: 工具名」、入参、状态)和 mcp_tool_call_log,并推进 agent_run 的进度;
  • 业务 ID 写进提示词:用户提示词里直接写出任务 ID、用户 ID、画像 ID、岗位 ID 和工具调用参数提示,模型调用画像读取、岗位读取工具时有明确的 ID 可传。

《AI Agent智能教务排课与教学质量分析系统》的排课在后台异步线程执行:Controller 在请求线程里先取出操作人 ID 传给独立的异步 Service,异步方法把它放进 TokenUtils 的 ThreadLocal,finally 里清掉;当前任务 ID 也绑定到线程上,模型调用写入工具漏传 taskId 时由 resolveCurrentTaskId 补上。

《AI 多Agent智能相亲交友匹配平台》用一个 ToolContext 数据类带运行期信息:运行编号、发起调用的角色、匹配任务、申请方会员编号、当前调度轮次、单次返回行数上限,由执行器构造好传给工具实现,工具只读不改,这些字段都不出现在工具的参数定义里。写库工具的归属也全从这里取:候选写进哪个任务、审核结论记在第几轮,模型只决定写什么内容。

读申请方择偶偏好的工具吃过一次亏:它早先收一个可选的会员编号参数,会员昵称取得像数字时(比如「2222」),模型会把昵称当编号传进来,读不到偏好,整轮筛选一个候选都筛不出来。后来这个工具去掉了参数,申请方是谁只从运行上下文里取。

为什么这样取舍

  • 业务 ID 和运行 ID 分两条路走:任务、用户、画像、岗位 ID 是模型调工具时要传的业务参数,所以写进用户提示词;运行 ID 只用来把步骤和调用日志挂到这次运行上,由调用方放进工具上下文,不出现在工具的参数定义里。
  • 排课要跨线程传操作人:异步线程读不到 token,AI 调用日志和工具调用日志的操作人一列会全部为空。

面试官还会追问

  • 管理端在工具管理页手动执行一个工具时没有工具上下文,这次调用的日志和 Agent 步骤怎么处理?
  • 工具注册表里的入参说明写的不是合法 JSON 时,这个工具还能交给模型吗?参数类型又是怎么推断出来的?

学完《AI Agent岗位匹配与求职规划系统》,上面这些追问你都会迎刃而解。

本题落地项目地狱锤炼AI Agent岗位匹配与求职规划系统基于简历解析、JD结构化抽取、MCP工具调用和Agent多步骤执行,实现岗位匹配评分、能力差距诊断、项目包装建议、改进计划和求职规划报告,适合岗位样本、职业规划闭环和Agent执行追踪。SpringbootSpringAIMCPAgentRAG源码+SQL喂饭学习教程配套面试文档环境安装文档项目运行文档 学习这个项目 也可以学AI Agent智能教务排课与教学质量分析系统地狱锤炼 查看项目 也可以学AI 多Agent智能相亲交友匹配平台地狱锤炼 查看项目