常见问题
概述
本页帮助你处理使用 Agent 时最常见的疑问和故障。例如:任务创建后为什么没有运行、怎样查询最终结果、 为什么工具没有被调用、审批后怎样继续,以及应用重启后如何找回任务。
不需要先了解框架内部结构。你可以根据遇到的现象直接选择下面的分类:
| 遇到的情况 | 查看章节 |
|---|---|
不清楚 Agent 的作用,或不知道该用 run(...) 还是 start(...) | 基础使用 |
| Agent 忘记上一轮内容,或对话越来越慢 | 对话与上下文 |
| 工具没有调用、审批没有生效、任务等待输入 | 工具与人工交互 |
| 后台任务不执行,或重启后找不到任务 | 后台任务与持久化 |
| 任务失败、超时、没有重试或长时间不结束 | 错误与排查 |
| 准备将 Agent 部署到正式环境 | 生产环境检查清单 |
如果问题与某一项任务有关,请先保存它的 turnId。它相当于任务编号,可用于查询最新状态、提交审批或 输入,以及关联日志。
基础使用
Agent 和 ChatModel 有什么区别?
ChatModel 负责向大模型发送一次请求并返回结果。Agent 在模型之外,还定义了任务指令、可用工具和 执行规则,让模型能够完成查询、计算或业务操作。
如果只需要“一问一答”,直接使用 ChatModel 即可。如果任务需要调用工具、等待人工操作、后台运行或失败 后重试,更适合使用 Agent。Agent 的完整介绍见 Agent。
run(...) 和 start(...) 应该选哪个?
| 方法 | 会发生什么 | 适用场景 |
|---|---|---|
run(...) | 创建任务,并在当前线程执行到完成、失败或需要等待 | 几秒内完成的接口、本地程序 |
start(...) | 只创建任务并返回任务 ID,不立即执行 | 长任务、需要接口快速返回的任务 |
使用 start(...) 后,需要由业务自己的线程池、消息队列或调度器显式调用 Runner 执行。不要把可能运行几分钟的任务放在 HTTP 请求线程中一直等待。执行入口的完整说明见 AgentRunner。
为什么调用 start(...) 后任务一直是 READY?
这是 start(...) 的正常行为。READY 表示任务已经创建,正在等待执行,并不表示发生了错误。
需要依次确认:
- 是否有业务线程、消息消费者或调度器调用 Runner;
- 调度代码和 Runner 是否连接同一个
AgentTurnStore; AgentLoader是否能加载该任务对应的 Agent 版本;- 执行线程是否仍在运行,并且还有可用的并发执行名额。
如果不需要异步执行,可以直接使用 runner.run(...)。
为什么 run(...) 返回后不是 COMPLETED?
run(...) 会一直执行到“当前可以停下的位置”,不保证每次都正常完成。任务可能正在等待用户输入、工具 审批、外部工具结果或下次重试时间,也可能已经失败、被取消或达到运行限制。
先读取 turn.getStatus()。等待状态需要提交对应结果,失败和限制状态则需要查看错误或调整配置。各种状态 的含义见 AgentTurn。
怎样根据 turnId 查询状态和最终结果?
使用创建任务时返回的 turnId 重新读取最新任务:
AgentTurn turn = runner.restore(turnId);
System.out.println("当前状态:" + turn.getStatus());
if (turn.getStatus() == AgentTurnStatus.COMPLETED) {
System.out.println("最终结果:" + turn.getFinalOutput());
}restore(...) 只查询任务,不会自动继续执行。只有状态为 COMPLETED 时,getFinalOutput() 才表示正常 完成后的最终答案。查询接口还应校验当前用户是否有权访问这个 turnId。
对话与上下文
新的用户消息应该创建新任务,还是调用 resume(...)?
新的问题或正常的下一轮聊天,应调用带相同 conversationId 的 run(...)。会话没有活跃 Turn 时,它会 创建新的 AgentTurn;活跃 Turn 正处于阻塞状态时,它会把消息追加到原 Turn 并重新规划。
审批、表单、模型重试或外部工具结果等类型明确的回调使用 resume(...)。业务明确要求创建全新 Turn 时 使用 start(...);如果会话仍有活跃 Turn,该方法会拒绝创建。详细区别见模型故障恢复。
下一轮对话需要手动读取并传入历史消息吗?
不一定。配置 ChatMemoryProvider 后,Runner 会根据 conversationId 自动读取历史消息,并在执行过程中 保存本轮新增消息:
AgentRunner runner = AgentRunner.builder()
.chatMemoryProvider(chatMemoryProvider)
.build();
AgentTurn turn = runner.run(
agent,
"conversation-1001",
"继续查询刚才的订单"
);没有配置 ChatMemoryProvider 时,Runner 无法自动获得之前的对话。连续对话的配置和消息范围见 上下文管理。
AgentTurnStore 和 ChatMemory 有什么区别?
| 组件 | 保存什么 | 解决什么问题 |
|---|---|---|
AgentTurnStore | 一项任务的状态、进度和等待信息 | 让同一项任务可以查询、恢复或由业务代码继续 |
ChatMemory | 同一会话的多轮聊天记录 | 让新的任务理解之前聊过什么 |
例如,退款任务正在等待审批,这个进度由 Store 保存;审批完成后,用户开启新一轮对话询问“退款到账了吗”, 历史聊天内容由 ChatMemory 提供。两者用途不同,不能互相替代。
为什么同一 conversationId 不能同时创建两个活动任务?
同一会话中如果两项任务同时写入历史,消息顺序可能混乱,后一项任务也可能在错误的上下文上回答。因此, 配置 ChatMemory 后,同一会话同时只允许一个尚未结束的 AgentTurn。
先完成、取消或恢复当前任务,再创建下一项任务。确实需要并行处理时,应使用不同的 conversationId。
上下文越来越长、响应越来越慢怎么办?
长对话会把越来越多的历史消息发送给模型,从而增加耗时和 Token 消耗。可以按以下顺序处理:
- 不要把日志、完整文档等大段内容直接作为工具结果返回;
- 为查询工具增加分页、筛选和摘要;
- 设置上下文窗口大小,避免无限保留历史消息;
- 开启上下文压缩,把较早内容整理成摘要,同时保留最近消息。
修改 Agent 后,尚未结束的任务会使用新版本吗?
不会自动切换。任务会记录创建时使用的 Agent ID 和版本,恢复时应继续加载同一版本,否则工具、指令和 执行规则发生变化,原任务可能无法正确继续。
发布新版本时,应保留仍有未完成任务的旧版本。版本加载方式见 Agent 加载与版本。
为什么恢复任务时提示找不到 Agent?
Store 保存的是任务进度,不会保存模型客户端和 Java 工具函数。恢复任务时,Runner 需要通过 AgentLoader 重新加载任务创建时使用的 Agent 版本。
请确认 Loader 已配置到 Runner,Agent ID 和版本一致,并且旧版本尚未被删除。多实例部署时,每个实例都 必须能够加载相同版本。
工具与人工交互
为什么模型没有调用已经配置的工具?
注册工具表示模型“可以使用”,并不保证它一定会调用。可以依次检查:
- 工具是否注册到了当前 Agent,而不是只在其他 Agent 中创建;
- 工具名称、用途和参数说明是否清楚;
- Agent 指令是否明确要求在相应场景使用工具;
- 当前模型是否支持工具调用(Tool Calling);
- 工具是否被当前的可见性规则隐藏;
- 用户的问题是否真的需要这个工具。
对于订单状态、库存等不能猜测的数据,应在指令中明确写出“必须调用工具,以工具结果为准”。仍有问题时, 通过 AgentEventListener 观察模型是否返回了工具调用请求。
为什么工具需要审批,但任务直接执行了?
最常见的原因是没有为当前 Agent 配置 toolApprovalPolicy(...)。未配置审批规则时,工具默认允许执行。
如果已经配置,请检查规则是否匹配实际工具名称或元数据,以及匹配后是否错误地返回了 ALLOW。审批只对 由 AgentRunner 调用的工具生效;业务代码绕过 Runner 直接调用工具函数,自然不会进入审批流程。完整示例 见人工审批。
审批、表单输入和普通追问有什么区别?
| 场景 | 含义 | 应该怎样处理 |
|---|---|---|
| 人工审批 | 参数已经确定,需要决定是否允许执行高风险操作 | 恢复原任务并提交批准或拒绝 |
| 表单输入 | 执行所需信息不完整,需要用户填写固定字段 | 恢复原任务并提交表单数据 |
| 普通追问 | 用户在上一 Turn 结束后开始新的问题 | 创建新的 AgentTurn |
| 阻塞时改口 | 原 Turn 尚未结束,用户修改要求或要求继续 | 向原 Turn 追加消息并重新规划 |
审批、表单和阻塞时改口都在继续原来的任务,因此必须保留原 turnId,不能重新创建任务代替恢复。
resume(...) 和 submitResume(...) 有什么区别?
两者都会向当前等待中的任务提交与等待原因匹配的结果,区别在于由谁继续执行:
| 方法 | 提交结果后 | 适用场景 |
|---|---|---|
resume(...) | 在当前线程立即继续,直到再次等待或结束 | 同步接口、提交后需要立即返回结果 |
submitResume(...) | 只将任务恢复为可执行状态 | 提交接口快速返回,由业务调度器后台继续 |
恢复内容包括用户输入、审批批准或拒绝、外部工具成功或错误结果、模型重试、继续执行和到期重试。 userMessage(...) 是普通消息,在 WAITING_FOR_USER 中优先回答当前请求,在其他等待中触发重新规划; replanWithMessage(...) 才表示无条件打断任意阻塞状态。其他命令必须与当前等待原因匹配,否则 Runner 会拒绝提交。 详细说明见挂起和恢复。
为什么任务一直等待表单、审批或外部工具?
等待状态不会自己消失,业务系统必须完成对应动作:
| 状态 | 正在等待 | 下一步 |
|---|---|---|
WAITING_FOR_USER | 用户文本或表单 | 提交 userInput(...) |
WAITING_FOR_APPROVAL | 工具审批 | 提交 approveTool(...) 或 rejectTool(...) |
WAITING_FOR_TOOL | 外部工具结果 | 提交 toolResult(...) 或 toolError(...) |
WAITING_FOR_MODEL | 模型额度、限流、Token 上限或服务异常 | 修复后提交 retryModel(...),或发送新消息 |
还要确认提交时使用了正确的 turnId 和当前等待事项的关联 ID。使用 submitResume(...) 后,业务代码还需要显式调用 Runner;否则它会保持可执行但不会继续。
为什么审批结果或表单数据提交后被拒绝?
通常是任务状态已经变化,或者提交的结果不属于当前等待事项。例如,用上一次审批的工具调用 ID 去恢复新的 审批请求,Runner 会拒绝,以免结果作用到错误操作。
提交前重新调用 restore(turnId) 读取最新状态和等待信息,不要长期缓存旧的任务对象。重复提交或迟到结果 应当按“已经处理”向用户返回,而不是改用另一个恢复命令强行继续。
工具为什么可能执行两次?
应用异常退出、业务代码重新推进任务、自动重试,以及表单提交后重新执行原工具,都可能让同一业务 动作再次到达工具代码。框架会保护任务进度,但无法撤回已经发送给支付、邮件或物流系统的请求。
退款、扣款、发货、发送消息等写操作必须支持幂等,也就是同一个业务请求即使重复到达,也只产生一次影响。 工具中可以使用 AgentToolContext.getIdempotencyKey() 获取稳定的幂等键,详见 工具运行上下文。
AgentToolContext 和 AgentToolResumeInfo 有什么作用?
它们供正在执行的 Java 工具读取本次任务的运行信息,不需要业务代码自行创建。
AgentToolContext提供任务 ID、工具调用 ID、执行次数、幂等键、已提交的表单数据和进度上报能力;AgentToolResumeInfo说明这次工具执行是否来自审批、表单恢复或错误重试,并记录恢复次数、附加信息和 上一次错误。
普通只读工具通常不需要使用它们。需要防止重复写入、读取表单数据、记录审批人或区分错误重试时,再引入 这些信息。完整示例见工具运行上下文。
后台任务与持久化
浏览器关闭和应用服务重启有什么区别?
浏览器关闭只会中断页面连接。只要应用服务中的执行线程仍在运行,后台任务可以继续;用户重新打开页面后, 根据 turnId 查询最新状态即可。
应用服务重启会结束当前进程。如果使用内存 Store,任务数据会丢失;如果使用共享的 JDBC 或 Redis Store, 并且重启后的应用能加载原 Agent 版本,业务代码可以在新请求到达时显式恢复或修复未结束的任务。
为什么应用重启后找不到任务?
最常见的原因是使用了默认的内存 Store。内存数据只存在于当前应用进程,重启后无法恢复。
生产环境应配置 JDBC 或 Redis AgentTurnStore,并确认重启前后的应用连接同一套存储。Redis 是否能在自身 重启后保留数据,还取决于 Redis 的持久化和备份配置。具体配置见任务快照持久化。
内存 Store 可以用于多实例部署吗?
不可以。每个应用实例都有自己的内存,实例 A 创建的任务无法被实例 B 查询或执行。
多实例必须使用所有实例共享的 JDBC 数据库或 Redis,并由业务层协调同一 Turn 的执行,并能够加载相同的 Agent 版本。
因此,涉及真实业务影响的工具仍要使用幂等键。可以把幂等理解为“同一请求重复执行,业务结果仍然只有 一份”。
保存任务时出现版本冲突是什么意思?
这表示当前代码准备保存的任务已经被其他请求或执行线程更新。框架通过版本号避免旧数据覆盖新数据,这种 “比较旧版本后再更新”的保护有时也称为 CAS。
遇到冲突时,应停止使用手中的旧对象,通过 restore(turnId) 重新读取最新任务,再根据最新状态决定是否 仍需操作。不要跳过版本检查直接覆盖数据。
错误与排查
怎样判断任务正在等待、执行失败还是已经结束?
先查看 AgentTurn.getStatus(),不需要从内部字段推测:
| 状态 | 含义 | 常见处理 |
|---|---|---|
READY、RUNNING | 等待执行或正在执行 | 检查业务执行线程、模型和工具耗时 |
WAITING_FOR_USER、WAITING_FOR_APPROVAL、WAITING_FOR_TOOL | 等待外部结果 | 展示对应操作并提交恢复命令 |
WAITING_FOR_MODEL | 等待模型条件恢复 | 检查 getModelFailure(),修复后重试或接收新消息 |
RETRY_SCHEDULED | 暂时失败,等待下次重试 | 检查重试时间和业务调度器 |
COMPLETED | 正常完成 | 读取 getFinalOutput() |
FAILED | 执行失败 | 读取错误并查看相关日志 |
CANCELLED | 已取消 | 向用户展示取消结果 |
MAX_ITERATIONS_REACHED、MAX_STEPS_REACHED、BUDGET_EXCEEDED | 达到运行限制 | 检查任务设计和预算配置 |
状态的完整生命周期见 AgentTurn。
为什么自动重试没有执行?
先确认错误被配置为“可以重试”,并且任务状态已经变为 RETRY_SCHEDULED。然后检查:
- 等待时间是否已经到达;
- 业务调度器是否在到期后显式推进;
- 调度器与 Runner 是否使用同一个 Store;
- 最大重试次数是否已经用完;
- 整个任务是否已经超过运行时间或预算限制。
自动重试需要由业务调度器在到期后调用 resume(...),不需要页面反复调用。
如何排查长时间处于 RUNNING 的任务?
RUNNING 可能表示模型请求或工具仍在进行,也可能是执行进程异常退出后尚未被修复。建议依次检查:
- 最近一条事件停在模型调用还是工具调用;
- 模型请求和工具是否配置了超时;
- 业务调度器是否仍在运行并能正常访问 Store;
- 是否只有一个实例卡住,是否需要业务代码在新请求到达时修复;
- 外部接口是否已经产生业务结果,避免人工重试造成重复操作。
不要只依赖浏览器中的实时消息判断。应以 restore(turnId) 读取的最新状态为准,排查方法见 可观测性。
Token 预算为什么没有按预期生效?
Token 数量依赖模型返回用量信息。如果模型实现没有提供用量,Runner 无法准确累计 Token。
生产环境不要只设置 Token 上限,还应同时限制模型迭代次数、总执行步骤、工具调用次数和总运行时间,并为 模型和工具设置超时。配置说明见运行限制与预算和超时与过期。
如何取消任务?
调用 runner.cancel(turnId)。取消采用协作方式:Runner 会在适合停止的位置结束任务,但不会强制终止已经 发出的 HTTP 请求或正在执行的 Java 方法。
用户点击“停止生成”时使用 runner.stop(turnId);如果需要等待本地模型流或工具执行退出,可使用 runner.stopAndWait(turnId, timeoutMillis)。该操作仍会持久化取消标记,跨进程执行时会退化为协作式取消。
如果本地 Tool 创建了子进程、HTTP 请求或其他外部资源,应通过 AgentToolContext.getCancellation().onStop(...) 注册资源清理动作,并在 Tool 自己的 finally 中确认资源退出。停止回调只负责发出取消信号,不能保证已经提交的 扣款、发货或消息发送等外部副作用被回滚。
因此,取消后仍应确认外部系统是否已经完成扣款、发送或发布等操作。取消一项任务后,任务状态会变为 CANCELLED,不能再通过恢复命令重新打开。
AgentEventListener 可以用于可靠审计吗?
不能单独承担可靠审计。AgentEventListener 默认在发布事件的线程中同步执行;通过 AgentRunnerOptions.eventExecutor(...) 配置执行器后,也可以异步分发。
无论同步还是异步,事件都不会由框架自动持久化,应用重启或监听器处理失败时可能丢失。页面进度、普通日志 和监控指标可以直接使用监听器;计费、合规审计等不能丢失的数据,应转发到业务审计库或可靠消息系统,并按 事件 ID 做重复处理保护。详细说明见 AgentEventListener。
挂起恢复后还会继续流式输出吗?
会。流式调用是当前 AgentTurn 的执行设置,审批、表单或外部工具等待结束后,继续执行时仍会沿用该设置。 但浏览器断线期间的增量内容不一定会重新推送,页面重连后应先查询任务状态和已保存结果,再继续接收新事件。
metadata 可以保存任意对象吗?
不建议。metadata 表示随任务保存的附加信息,只应存放体积小、结构稳定、能够序列化且不敏感的数据, 例如业务单号、操作来源和经过筛选的标签。 数据库连接、线程、本地回调、模型客户端、密码和 API Key 都不应放入 metadata。
自定义类型还要考虑应用升级后的兼容性。能用字符串、数字、布尔值和简单集合表达时,优先使用这些基础类型。
生产环境检查清单
在正式环境启用 Agent 前,至少确认以下事项:
- 使用共享的 JDBC 或 Redis
AgentTurnStore,不依赖进程内存保存任务; - 配置
AgentLoader,并保留未结束任务所依赖的历史 Agent 版本; - 需要连续对话时,配置所有实例共享的
ChatMemoryProvider; - 后台任务已配置业务线程池或消息调度器,并设置合理的总并发数;
- 模型请求和工具调用都有超时,任务设置了迭代、步骤、工具次数、Token 和运行时间限制;
- 根据错误类型配置自动重试,确认到期任务能够被业务调度器继续;
- 退款、扣款、发货、发送消息等写操作使用稳定的业务幂等键;
- 审批、表单、恢复、取消和任务查询接口都执行登录、权限及数据范围校验;
- 日志、事件、任务数据和表单数据在展示或外发前完成脱敏;
- 监控待执行任务、长期等待任务、失败率、重试积压、调度器状态和 Store 异常;
- 关键审计数据写入可靠存储,不只依赖进程内的 AgentEventListener;
- 应用关闭时先停止接收新任务,并为正在执行的步骤预留完成时间;
- 为任务数据、聊天记录和审计记录分别制定备份、保留与清理策略。
相关文档索引
| 主题 | 文档 |
|---|---|
| 认识 Agent 和完成第一个示例 | Agent、快速开始 |
| 创建、执行、恢复和取消任务 | AgentRunner |
| 查看任务状态和结果 | AgentTurn |
| 管理连续对话和长上下文 | 上下文管理、上下文压缩 |
| 控制工具执行方式 | 工具执行控制、工具运行上下文 |
| 处理人工操作和等待状态 | 人工审批、表单输入、挂起和恢复 |
| 执行后台任务 | 业务线程池、消息队列或调度器 |
| 保存任务和加载 Agent 版本 | 任务快照持久化、Agent 加载与版本 |
| 处理故障和运行限制 | 错误处理与重试、超时与过期、运行限制与预算 |
| 观察任务运行过程 | AgentEventListener、可观测性 |