LangChain4j 面试题:工具调用怎么控边界?@Tool、动态工具、权限审计讲透

LangChain4j 的工具调用做得很顺手,@Tool 一上去很多业务能力都能很快接起来。
但真正上线时,最难的不是工具本身,而是边界:哪些能查、哪些能写、哪些必须人工确认,哪些参数要系统注入而不是让模型自己猜。

🦅个人主页
🐼GitHub主页


先看真实问题:为什么 AI 一旦接到核心业务系统,边界问题会立刻冒出来

模型一旦能调业务工具,它就不再只是‘会回答’,而是开始影响真实系统状态。这时候最先要想清楚的不是效果,而是安全和可控。
LangChain4j 给了静态工具、动态工具、参数描述、默认值、立即返回这些能力,但你还是需要一层业务规则来兜住它。

  • 读工具和写工具的风险完全不一样,应该分层治理
  • 租户、操作者、traceId 这类上下文最好由系统注入,不要全靠模型传参
  • 每次工具调用都应该能查到参数、执行结果和失败原因

一张表先看懂:工具调用在真实项目里最容易被追问的几个点

维度 怎么做 为什么
@Tool 把 Java 方法暴露给模型 先让工具能力明确可见
@P 约束参数名、描述、默认值、可选性 减少模型瞎传参
ToolProvider 按当前用户和场景动态提供工具 避免每次都暴露全部工具
ReturnBehavior 让部分结果直接返回而不是回模型重处理 节省一次额外推理开销

举个具体例子:售后助手:客服能查订单和退款资格,但不能让模型直接发起退款

  1. 客服问‘订单 A20260528001 为什么不能退款’,模型可以调用查询订单和校验退款资格工具。
  2. 如果客服继续说‘那你直接帮我退’,模型最多只能生成申请建议,真正提交还要人工确认。
  3. 不同角色看到的工具也不一样,普通客服和主管权限不同。
  4. 所有工具调用结果都要落审计表,后续才能做误调分析和合规追踪。

企业里的典型应用场景

  • 售后中台助手:允许模型查订单、查物流、查退款资格,但不允许直接做退款写操作。
  • 运营助手:可以查询活动配置、营销预算,但提交活动上线仍然要走审批。
  • 财务辅助问答:可以查单据状态和对账结果,但不能直接冲销或确认打款。

如果按企业项目落地,我会这样走完整闭环

  1. 入口层先识别角色、租户和业务场景,决定这次调用理论上能看到哪些工具。
  2. ToolProvider 只暴露当前场景允许的工具,避免一个万能工具集被所有请求共享。
  3. 模型调用工具后,执行层负责真正的业务校验、幂等和权限复核,不信任模型单方面决策。
  4. 高风险写操作统一拆到人工确认链路,模型最多生成建议,不直接提交核心交易。
  5. 审计层记录工具名、参数、结果、操作者、traceId 和错误堆栈,方便合规排查。
  6. 治理层对误调用、超时、参数错误做回放分析,持续缩小工具暴露面。

代码示例:@Tool + 动态工具 + 审计日志

静态工具定义

public class AfterSaleTools {

    @Tool("根据订单号查询订单状态")
    public OrderView getOrder(
            @P(name = "orderNo", description = "订单号") String orderNo) {
        return orderQueryService.query(orderNo);
    }

    @Tool("校验当前订单是否允许退款")
    public RefundRuleResult checkRefund(
            @P(name = "orderNo", description = "订单号") String orderNo) {
        return refundRuleService.check(orderNo);
    }
}

按角色动态下发工具

ToolProvider toolProvider = request -> {
    String role = request.invocationParameters().get("role");
    if ("SUPERVISOR".equals(role)) {
        return ToolProviderResult.from(List.of(
                ToolSpecifications.toolSpecificationsFrom(new AfterSaleTools()),
                ToolSpecifications.toolSpecificationsFrom(new RefundApproveTools())
        ));
    }
    return ToolProviderResult.from(
            ToolSpecifications.toolSpecificationsFrom(new AfterSaleTools())
    );
};

AI Service 装配和审计

public interface AfterSaleAssistant {
    String chat(@UserMessage String question, InvocationParameters parameters);
}

AfterSaleAssistant assistant = AiServices.builder(AfterSaleAssistant.class)
        .chatModel(chatModel)
        .toolProvider(toolProvider)
        .registerListener(new ToolExecutedAuditListener())
        .build();

InvocationParameters parameters = InvocationParameters.from(Map.of(
        "role", "CSR",
        "tenantId", "tenant_a",
        "operatorId", "u1001"
));

写操作必须走人工确认链路

public RefundApplyResult submitRefund(RefundCommand command, boolean humanConfirmed) {
    if (!humanConfirmed) {
        throw new IllegalStateException("退款提交必须人工确认后才能执行");
    }
    return refundApplicationService.submit(command);
}

企业级代码示例:企业里真正可上线的是 ToolFacade + Policy + Approval 的组合

工具执行网关

@Service
@RequiredArgsConstructor
public class ToolExecutionGateway {

    private final ToolAccessPolicyService toolAccessPolicyService;
    private final ToolAuditLogRepository toolAuditLogRepository;
    private final RefundApplicationService refundApplicationService;

    public ToolExecuteResult executeRefundApply(RefundApplyCommand command) {
        toolAccessPolicyService.assertAllowed(
                command.tenantId(),
                command.operatorId(),
                "REFUND_APPLY"
        );

        if (!command.humanApproved()) {
            return ToolExecuteResult.reject("退款申请必须人工确认后才能提交");
        }

        try {
            RefundApplyResult result = refundApplicationService.submit(command);
            toolAuditLogRepository.save(ToolAuditLogEntity.success(
                    command.traceId(),
                    "REFUND_APPLY",
                    JsonUtils.toJson(command),
                    JsonUtils.toJson(result)
            ));
            return ToolExecuteResult.success(result);
        } catch (Exception ex) {
            toolAuditLogRepository.save(ToolAuditLogEntity.fail(
                    command.traceId(),
                    "REFUND_APPLY",
                    JsonUtils.toJson(command),
                    ex.getMessage()
            ));
            throw ex;
        }
    }
}

SQL 示例:工具调用审计表

create table ai_tool_audit_log (
    id bigint primary key auto_increment,
    session_code varchar(128) not null,
    tool_name varchar(128) not null,
    operator_id varchar(64) null,
    role_code varchar(64) null,
    args_json json null,
    result_json json null,
    success_flag tinyint not null,
    error_message varchar(500) null,
    created_time datetime not null default current_timestamp
);

系统设计时我会优先拆哪几层

工具声明层

  • 工具名称、参数名、参数描述越清楚,模型选错工具的概率越低
  • 写业务工具时尽量别暴露万能入口,要拆成语义清晰的小能力

权限与上下文层

  • 同一个场景下,不同角色拿到的工具集合可以完全不同
  • 租户、操作者、角色这些上下文尽量通过 InvocationParameters 注入

审计和幂等层

  • 所有工具调用都留日志,高风险写操作还要带人审标记
  • 会改变系统状态的操作必须有幂等设计,否则模型重试可能造成副作用

真正上线时最容易卡住的点

  1. 把所有工具一股脑都暴露给模型,表面上最灵活,实际上最危险。
  2. 工具参数不写描述,模型就更容易选错字段或猜错语义。
  3. 没有工具审计表,一旦出现误操作,根本不知道是哪次调用出的事。

监控和指标建议盯哪些

  • 工具调用成功率和失败原因分布
  • 高风险工具的人审触发率
  • 模型选错工具率
  • 按角色维度统计的工具调用量

如果面试官问我这块怎么设计,我会这样答

如果面试官问 LangChain4j 的工具调用怎么控边界,我会先把问题拆成三层:工具怎么暴露、权限怎么控制、日志怎么审计。项目里我会把查询工具和写工具分开,写工具默认需要人工确认;再用 ToolProvider 按角色动态下发工具;最后配一层审计日志和幂等保护。这种回答会比只说 @Tool 更像真实项目经验。

结语

工具调用真正难的地方,不在于模型能不能调,而在于你有没有守住业务系统的边界。

如果是你来做,会不会让模型直接执行退款或审批?这题很适合拿来面试反问自己。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐