跳到主要内容

表单输入 ​

概述 ​

用户提出任务时,经常不会一次提供所有必要信息。例如:

  • 预定会议室时没有说明会议时间和参会人数;
  • 创建故障工单时缺少受影响系统和影响范围;
  • 申请报销时没有填写费用类型、金额或发票信息;
  • 预约服务时没有选择日期、地点和联系方式。

对于简单问题,Agent 可以直接在对话中追问。但当需要收集多个字段、限制可选值或检查必填项时,使用 表单通常更加清晰。页面可以将字段显示为输入框、下拉框或数字输入框,用户提交完整信息后,Agent 再 继续原来的任务。

表单输入主要解决三个问题:

  1. 明确告诉用户还需要填写哪些信息。
  2. 使用固定字段和选项,减少格式错误与理解歧义。
  3. 在用户填写期间保存当前任务,提交后从原任务继续。

表单只负责收集信息,不代表相关业务操作已经执行。例如,填写退款申请不等于退款成功;涉及资金、 删除或发布等高风险操作时,还应根据业务要求配置人工审批。

适用场景 ​

场景适合收集的字段推荐控件
会议室预定主题、时间、参会人数文本框、日期时间、数字输入框
故障工单受影响系统、影响范围、错误描述文本框、下拉框、多行文本框
报销申请费用类型、金额、发生日期下拉框、金额输入框、日期选择器
服务预约服务类型、预约时间、联系方式下拉框、日期时间、文本框

如果只缺少一个简单值,并且不要求固定格式,普通对话追问可能更自然。需要多个字段、明确选项或前后端 统一校验时,再使用表单。

快速开始 ​

下面以“预定会议室”为例。用户只说“帮我预定会议室”时,Agent 会先请求用户填写会议主题、期望时间 和参会人数,收到表单后再调用会议室预定工具。

示例中的 chatModel 表示已经创建好的大模型客户端,meetingService 表示应用已有的会议室服务。

1. 定义表单字段 ​

表单使用 JSON Schema 描述。下面通过 Java Map 定义三个字段:

java
Map<String, Object> subject = new LinkedHashMap<>();
subject.put("type", "string");
subject.put("title", "会议主题");

Map<String, Object> preferredTime = new LinkedHashMap<>();
preferredTime.put("type", "string");
preferredTime.put("title", "期望时间");
preferredTime.put("description", "例如:明天下午 3 点");

Map<String, Object> participantCount = new LinkedHashMap<>();
participantCount.put("type", "integer");
participantCount.put("title", "参会人数");
participantCount.put("minimum", 1);

Map<String, Object> properties = new LinkedHashMap<>();
properties.put("subject", subject);
properties.put("preferredTime", preferredTime);
properties.put("participantCount", participantCount);

Map<String, Object> meetingSchema = new LinkedHashMap<>();
meetingSchema.put("type", "object");
meetingSchema.put("title", "填写会议安排");
meetingSchema.put("properties", properties);
meetingSchema.put("required", Arrays.asList(
    "subject", "preferredTime", "participantCount"));

常用的 Schema 配置如下:

配置作用
type: object表示整份表单由多个字段组成
properties定义表单包含哪些字段
字段的 type定义数据类型,例如 string、integer 或 boolean
字段的 title页面上展示的字段名称
字段的 description补充填写提示
required列出必须填写的字段
minimum设置数字字段的最小值,本例要求参会人数至少为 1

需要提供固定选项时,可以使用 enum:

java
impactScope.put("enum", Arrays.asList(
    "ONE_USER", "PARTIAL_USERS", "ALL_USERS"));

前端可以据此显示下拉框,后端也应使用同样的选项校验提交值。

2. 创建并注册表单 ​

java
AgentFormDefinition meetingForm = AgentFormDefinition
    .builder("meeting_room_booking")
    .description("预定会议室时收集会议主题、时间和参会人数")
    .schema(meetingSchema)
    .build();

Tool userInputTool = AgentUserInputTool.builder()
    .form(meetingForm)
    .build();
配置作用
builder("meeting_room_booking")设置表单的稳定标识;同一个 Agent 中不能重复
description(...)说明表单适用于什么情况,帮助 Agent 选择正确表单
schema(...)指定表单字段、数据类型和校验规则
AgentUserInputTool.builder()创建框架提供的表单请求工具
form(meetingForm)将表单注册到该工具;也可以连续注册多份表单

AgentUserInputTool 不会执行会议室预定。它只允许 Agent 在缺少信息时请求一份已经注册的表单。表单的 字段结构由应用提前定义,模型不能临时增加或修改字段。

3. 定义后续业务工具 ​

用户提交表单后,Agent 需要调用真正的会议室预定工具:

java
Tool reserveMeetingRoom = Tool.builder(
        "reserve_meeting_room",
        "根据完整的会议资料预定会议室")
    .addParameter(Parameter.builder()
        .name("subject")
        .type("string")
        .description("会议主题")
        .required(true)
        .build())
    .addParameter(Parameter.builder()
        .name("preferredTime")
        .type("string")
        .description("会议时间")
        .required(true)
        .build())
    .addParameter(Parameter.builder()
        .name("participantCount")
        .type("integer")
        .description("参会人数")
        .required(true)
        .build())
    .function(arguments -> meetingService.reserve(arguments))
    .build();

表单负责收集数据,reserve_meeting_room 负责执行业务操作。两者分开后,即使用户尚未填写完整信息, 预定服务也不会被提前调用。

4. 创建 Agent ​

java
Agent agent = Agent.builder("meeting-agent")
    .instructions(
        "用户要求预定会议室但信息不完整时,调用 request_user_input,"
            + "并选择 meeting_room_booking。收到表单数据后,"
            + "必须调用 reserve_meeting_room,不得直接声称预定成功。")
    .chatModel(chatModel)
    .tool(userInputTool)
    .tool(reserveMeetingRoom)
    .build();
配置作用
instructions(...)告诉 Agent 何时请求表单,以及收到数据后应执行什么操作
chatModel(...)指定负责理解用户要求和选择工具的大模型
tool(userInputTool)让 Agent 可以请求用户填写表单
tool(reserveMeetingRoom)让 Agent 可以在资料完整后执行会议室预定

指令中应明确说明“不能直接声称成功”。模型生成的文字不是业务执行结果,真正的预定结果必须来自业务 工具。

5. 执行任务并读取表单 ​

java
AgentRunner runner = new AgentRunner();
AgentTurn waiting = runner.run(agent, "帮我预定一个会议室");

if (waiting.getStatus() == AgentTurnStatus.WAITING_FOR_USER) {
    String turnId = waiting.getId();
    String inputId = waiting.getSuspension().getCorrelationId();
    String formKey = waiting.getSuspension().getFormKey();
    Map<String, Object> schema = waiting.getSuspension().getSchema();

    // 将 turnId、inputId、formKey 和 schema 返回给前端。
}

AgentRunner 是任务执行器。当状态为 WAITING_FOR_USER 时,表示当前任务正在等待用户填写信息,后续 业务工具尚未执行。

数据用途
turnId标识当前任务,提交表单时需要使用
inputId标识当前输入请求,防止数据提交到其他表单
formKey标识页面正在填写哪一种业务表单
schema描述页面字段和校验规则,前端可据此渲染表单

new AgentRunner() 使用进程内存保存任务,只适合本地学习。生产环境中的表单通常跨越多个请求,需要 配置持久化存储,具体方式见 任务快照持久化。

6. 提交表单并继续任务 ​

用户填写表单后,将数据整理为与 Schema 字段一致的 Map:

java
Map<String, Object> formData = new LinkedHashMap<>();
formData.put("subject", "项目周会");
formData.put("preferredTime", "明天下午 3 点");
formData.put("participantCount", 8);

AgentTurn result = runner.resume(
    turnId,
    AgentResumeCommand.userInput(inputId, formData)
        .withMetadata("submittedBy", "user-1001")
);
配置作用
formData保存用户填写的字段和值,字段名必须与 Schema 一致
userInput(inputId, formData)将数据提交给对应的输入请求
resume(turnId, ...)提交数据后,在当前线程继续原任务
withMetadata(...)附加提交人、提交渠道等信息,为后续审计提供上下文

提交前,后端必须检查用户身份、字段类型、必填项、允许值和业务权限。前端校验主要用于改善填写体验, 不能代替后端校验。

注册多份表单 ​

一个 Agent 可以处理多种输入场景。例如,同时支持会议室预定和故障工单:

java
Tool userInputTool = AgentUserInputTool.builder()
    .form(meetingForm)
    .form(supportTicketForm)
    .build();

每份表单必须使用不同的 formKey,并提供清楚、不重叠的 description。Agent 会根据这些说明选择表单, 因此不应使用“表单一”“其他信息”之类含义不明确的描述。

还应在 Agent 指令中写清楚各表单的使用条件:

java
.instructions(
    "预定会议室缺少资料时选择 meeting_room_booking;"
        + "创建故障工单缺少资料时选择 support_ticket_details;"
        + "不得猜测用户没有提供的字段。")

同步与异步提交 ​

resume(...) 会在提交表单的当前线程中继续任务,适合命令行程序、内部服务或执行时间较短的接口。

Web 表单接口通常需要尽快返回,可以使用:

java
runner.submitResume(
    turnId,
    AgentResumeCommand.userInput(inputId, formData)
        .withMetadata("submittedBy", "user-1001")
        .withMetadata("requestId", "request-789")
);

submitResume(...) 只提交数据,不在当前请求中继续调用模型或执行业务工具。之后需要由已配置的 业务线程、消息队列或调度器在收到表单后显式调用 runner.resume(turnId, command) 继续任务。

方式提交后是否立即继续任务适用场景
resume(...)是本地程序、内部服务、短任务
submitResume(...)否Web 接口、后台任务、长任务

前端渲染 ​

前端可以根据标准 JSON Schema 选择控件:

Schema 配置常见页面控件
type: string文本输入框
type: string 加 enum下拉框或单选框
type: integer / number数字输入框
type: boolean复选框或开关
description字段下方的填写提示
required必填标记与提交校验

如果已经配置聊天记录,框架还可以通过 AgentFormMessage 提供待填写、已提交或已取消等页面状态。它是 一种可选的页面集成方式;只使用等待任务返回的 schema、turnId 和 inputId 也可以实现表单页面。

页面不应允许用户修改 Schema,也不应根据 Schema 执行 HTML、JavaScript 或其他表达式。提交成功后应 禁用重复提交,并以服务端返回的任务状态为准。

AgentFormRequiredException ​

概述 ​

AgentFormRequiredException 是本地 Tool 主动请求用户填写表单的控制流异常。它适用于 Tool 必须先完成 只读查询,才能确定是否缺少信息、需要哪些字段,或者应该展示哪一份表单的场景。

普通本地 Tool 无需声明额外属性。Tool 在产生任何副作用之前抛出携带 AgentFormDefinition 的 AgentFormRequiredException。Runner 捕获后会保留当前 ToolCall,创建标准用户输入挂起点,并将表单 定义交给业务系统。用户提交后,Runner 从头重新执行同一个 Tool,而不是从抛出异常的下一行继续。

AgentFormRequiredException 继承自抽象基类 AgentToolSuspensionException,但只持有表单定义,不包含 审批决定。它与 AgentApprovalRequiredException 分别表示表单输入和人工审批,两者不能相互替代。

作用 ​

AgentFormRequiredException 主要解决以下问题:

  1. 运行时确定字段:字段取决于客户类型、地区规则、产品配置或其他只读查询结果。
  2. 选择不同表单:同一个 Tool 可以根据业务状态选择企业、个人或其他场景的表单。
  3. 复用当前 ToolCall:提交数据直接回到发起请求的 Tool,不需要先交给模型再生成一次新的 ToolCall。
  4. 支持多次补充:Tool 可以根据已经提交的数据继续请求其他表单,Runner 会合并各次提交的字段。
  5. 复用统一协议:继续使用已有的挂起、超时、恢复、持久化和输入事件,不需要建立另一套等待机制。

适用场景 ​

场景为什么需要在 Tool 内请求表单可能补充的字段
客户开户查询客户类型后才能确定资料要求企业税号、统一社会信用代码或个人证件号
跨地区业务查询地区规则后才能确定必填项州、省、市、税务身份或本地许可信息
故障处理读取系统类型和诊断结果后才能确定问题清单日志范围、错误时间、受影响组件
商品定制查询产品配置后才能确定可选参数尺寸、材质、颜色或兼容型号
分阶段资料收集后续字段依赖前一份表单的答案补充证明、联系人或确认选项

如果在 Tool 执行前就能确定缺少哪些信息,应优先使用前文的 AgentUserInputTool,让 Agent 主动请求已经 注册的表单。如果只缺少一个简单值且没有结构化校验要求,直接通过对话追问通常更自然。外部 Tool 在 Runner 进程中没有本地函数可以抛出异常,因此不能使用 AgentFormRequiredException。

执行流程 ​

  1. 本地 Tool 执行可重复的只读查询,并读取 AgentToolContext.getSubmittedFormData()。
  2. Tool 根据业务规则和已有数据判断是否仍需补充信息。
  3. 如果需要,Tool 抛出携带对应表单定义的 AgentFormRequiredException。
  4. Runner 挂起当前 ToolCall,业务系统展示表单并通过 AgentResumeCommand.userInput(...) 提交数据。
  5. Runner 从头重新执行原 Tool;Tool 再次查询规则并校验提交数据。
  6. 如果数据完整,Tool 继续执行;如果仍缺少其他信息,可以再抛出另一份表单。

示例代码 ​

下面的 Tool 必须先查询客户类型,才能决定收集企业资料还是个人资料:

java
import com.agentsflex.agent.exception.AgentFormRequiredException;
import com.agentsflex.agent.tool.AgentToolContext;

Tool prepareCustomerTool = Tool.builder(
        "prepare_customer",
        "检查客户资料并整理开户所需信息")
    .function(arguments -> {
        AgentToolContext context = AgentToolContext.current();
        String customerId = String.valueOf(arguments.get("customerId"));

        // 该查询必须只读且可重复执行,因为提交表单后整个 Tool 会从头执行。
        CustomerType customerType = customerService.findType(customerId);
        Map<String, Object> submitted = context.getSubmittedFormData();

        if (customerType == CustomerType.ENTERPRISE
            && !containsEnterpriseDetails(submitted)) {
            throw new AgentFormRequiredException(enterpriseCustomerForm);
        }
        if (customerType == CustomerType.INDIVIDUAL
            && !containsPersonalDetails(submitted)) {
            throw new AgentFormRequiredException(individualCustomerForm);
        }

        // 对提交数据完成服务端校验后,才整理后续开户所需资料。
        validateCustomerDetails(customerType, submitted);
        return customerService.prepare(customerId, customerType, submitted);
    })
    .build();

enterpriseCustomerForm 和 individualCustomerForm 是应用预先构造的 AgentFormDefinition。它们应使用 不同且稳定的 formKey,并通过 JSON Schema 定义各自字段。Tool 必须检查 submitted 是否已经满足当前 表单要求;如果每次执行都无条件抛出同一异常,任务将无法继续。

请求表单必须发生在写数据库、扣费或调用外部写入接口之前。更稳妥的做法是让该 Tool 只负责查询和整理 资料,再由另一个受审批策略和幂等机制保护的 Tool 执行真正的业务写入。

读取工具的表单恢复信息 ​

上面示例中的 AgentToolContext 不只提供表单数据,还能说明当前工具是否因为表单提交而再次执行:

java
AgentToolContext context = AgentToolContext.current();
AgentToolResumeInfo resumeInfo = context.getResumeInfo();

if (context.isFormInputResumed()) {
    Map<String, Object> submitted =
        context.getSubmittedFormData();
    int resumeCount = resumeInfo.getResumeCount();
    int executionAttempt = context.getExecutionAttempt();

    // 校验 submitted,并记录本次恢复和执行次数。
}

第一次进入工具函数时,尚未提交表单:

信息第一次执行用户提交后的执行
getSubmittedFormData()空 Map返回已经提交并合并的字段
isFormInputResumed()falsetrue
isReplay()falsetrue
getExecutionAttempt()1从 2 开始递增
getResumeInfo().getType()NONEFORM_INPUT
getResumeInfo().getResumeCount()0每次恢复后递增

AgentToolResumeInfo 是 Runner 为当前工具准备的只读恢复说明,业务代码不需要自行创建。多次请求表单时, getSubmittedFormData() 会合并此前提交的字段,同名字段使用最近一次提交的值。

这组信息只适用于“业务工具抛出 AgentFormRequiredException”的方式。前文使用的 request_user_input 会先把表单数据交回模型,再由模型调用后续业务工具;后续工具是一次新的工具调用, 不会把这份数据放入它的 getSubmittedFormData()。后续工具应从模型生成的工具参数中读取所需字段。

方式适用条件推荐程度
Agent 主动请求已注册表单执行前就能判断缺少哪些信息默认选择
业务工具运行时请求表单必须查询业务规则后才能确定字段仅在确有需要时使用

关于工具恢复后的上下文和防重复执行方式,请参考 工具运行上下文。

表单输入与人工审批 ​

表单输入和人工审批可能连续出现,但解决的问题不同:

能力解决的问题示例
表单输入执行任务所需的信息不完整补充退款原因和收款账户
人工审批信息已经完整,但操作需要获得授权主管确认是否允许退款

一个退款流程可以先让用户补全资料,再生成明确的退款操作,最后交给有权限的人员审批。不能用“用户已 填写表单”代替操作授权,也不能用审批页面代替字段校验。

如果是否需要审批只能在 Tool 完成只读预检后确定,可以抛出 AgentApprovalRequiredException 主动申请 审批。它会复用标准工具审批的挂起和恢复协议,但批准后同样会从头执行原 Tool。Tool 应重新构造当前 ToolApprovalDecision,通过 AgentToolContext.isToolApproved(decision) 校验批准是否仍与本次只读 预检快照匹配。普通 Tool 无需声明额外元数据;并行批次中,Tool 主动审批仅保护 当前 Tool 自身,批次级前置审批仍应使用 toolApprovalPolicy。完整用法见 人工审批:AgentApprovalRequiredException。

同一 ToolCall 需要财务、合规等多级批准时,可以依次构造不同的 ToolApprovalDecision 并执行上述 检查。Runner 会按请求指纹累计批准记录,后一级批准不会覆盖前一级;任何一级被拒绝都会终止该 ToolCall。

安全要求 ​

  1. 后端必须重新校验字段类型、必填项、长度、范围和枚举值,不能只依赖前端校验。
  2. 提交接口必须校验登录用户、租户、任务状态和 inputId。
  3. Schema 与表单数据中不要包含密码、密钥等不应长期保存的敏感信息。
  4. 重复点击或重复回调只能产生一次有效提交,业务系统应使用 requestId 等唯一标识防重。
  5. 页面只渲染受信任的控件,不执行 Schema 中的脚本、HTML 或表达式。
  6. 表单提交成功不代表业务操作成功,最终结果应以业务工具的真实返回值为准。

相关文档 ​