APUS-OpenJev-v1 接入现有业务系统——先划清决策建议和执行动作的边界

文章导读
接入 APUS-OpenJev-v1 时,真正要先定下来的不是接口能不能调通,而是两件事:调用层怎么设超时和入口,消费层怎么对待返回内容。常见的坑是把模型输出的 decision 字段直接拿去执行——放行、拒绝、发通知,中间没有任何校验;一旦返回超时、JSON 被截断、字段类型变了,业务链路就跟着一起卡住或做出错误动作。比较稳妥的做法是让模型只产出建议和理由,由业务侧的规则层决定最终动作,再把超时
📋 目录
  1. 壹 在配置里固定调用入口与超时时间
  2. 贰 约定返回字段结构与缺失值处理
  3. 叁 写一段异常捕获覆盖超时与格式错误
  4. 肆 构造一次异常请求验证兜底是否生效
  5. 伍 在日志里留下决策来源标记
A A

接入 APUS-OpenJev-v1 时,真正要先定下来的不是接口能不能调通,而是两件事:调用层怎么设超时和入口,消费层怎么对待返回内容。常见的坑是把模型输出的 decision 字段直接拿去执行——放行、拒绝、发通知,中间没有任何校验;一旦返回超时、JSON 被截断、字段类型变了,业务链路就跟着一起卡住或做出错误动作。比较稳妥的做法是让模型只产出建议和理由,由业务侧的规则层决定最终动作,再把超时、异常和降级写进调用封装里。

接入的稳定点不在模型侧,而在调用层和消费层。调用层用配置固定入口、连接超时和读取超时,把最坏耗时压在一次业务请求能接受的范围里;消费层先校验字段结构和类型,缺字段、类型不符、解析失败一律走兜底,不把模型输出当作可执行指令。这三件事都可以通过改配置、看日志、构造一次异常请求来验证,不需要额外的压测环境。

下面按“先约定形态、再谈消费”的顺序展开,每一节都给出可替换的配置、代码骨架或验证动作,不写死任何具体接口地址。

在配置里固定调用入口与超时时间

超时不要写在调用代码里,否则改一次要重新发版。通常放在配置中心或本地配置文件,敏感值(密钥、令牌)走环境变量注入。入口地址也只保留一个占位符,方便不同环境切换。

# 位置示例:application.yml / config.yaml / 配置中心的 openjev 分组
openjev:
  client:
    endpoint: ${OPENJEV_ENDPOINT}      # 由环境变量注入,代码里不写死
    connect-timeout-ms: 300            # 建连超时,通常小于读取超时
    read-timeout-ms: 1500              # 单次读取超时
    total-timeout-ms: 2000             # 整个调用的上限,包含重试
    max-retries: 0                     # 先关重试,避免超时被叠加
    worker-pool-size: 8                # 并发调用线程数,按业务线程池余量给

几个判断:total-timeout-ms 应该小于上游业务接口的超时,否则模型侧还没返回,业务侧已经先超时了;max-retries 先设 0,确认链路上限之后再决定是否放开;并发线程数不要和业务主线程池共用,避免慢调用把业务线程占满。

验证方式:把 read-timeout-ms 临时改成 50,跑一次正常请求,观察调用是否按预期抛出超时异常、日志耗时是否接近 50ms 而不是一直挂着。也可以用一段脚本单独打调用封装,测完记得改回原值。具体参数名要结合项目使用的客户端库确认,不同库对“连接超时”和“读取超时”的命名不一致。

约定返回字段结构与缺失值处理

业务侧要能稳定解析结果,就得先约定一份最小字段清单,并且规定字段缺失时怎么走。下面是一份通用示例,字段名按实际情况替换:

{
  "request_id": "req-0001",
  "status": "ok",
  "decision": "review",
  "confidence": 0.62,
  "reason": "命中规则 A,建议人工确认",
  "model_version": "APUS-OpenJev-v1",
  "latency_ms": 812
}
  • status:只认 ok,其他值(含空值)直接走兜底。
  • decision:限定在白名单枚举内,出现白名单之外的取值一律降级为 review,不要当成 allow 执行。
  • confidence:期望是数值。如果是字符串 "0.62",可以尝试宽松转换;转换失败就当作缺失,不参与自动化判断。
  • reason:缺失时填固定文案,比如 no_reason,不要让下游拿到 null。
  • latency_ms、model_version:只用于日志和排查,缺失不影响主流程。
  • 未知字段:忽略,不要因为多了字段就抛解析异常。

分支处理建议写成一段独立的校验函数,调用方只拿校验后的对象。校验顺序通常是:JSON 能否解析 → status 是否合法 → decision 是否在枚举内 → 数值字段类型是否符合。任何一步不通过就返回兜底对象,并带上失败原因,而不是抛出异常让上层处理。

写一段异常捕获覆盖超时与格式错误

封装入口要把超时、连接失败、解析失败都收在同一个位置,保证异常不会穿透到主流程。下面是通用骨架,占位符需要替换:OpenJevClient、DecisionParser、Decision 换成项目里实际的类名,TIMEOUT_MS 从配置读取,traceId 由上游请求头或网关传入,不要在这里新生成。

APUS-OpenJev-v1 接入现有业务系统——先划清决策建议和执行动作的边界
public Decision handle(Input input, String traceId) {
    long start = System.currentTimeMillis();
    String source = "model";
    String errorType = "none";
    Decision result;
    try {
        String body = openJevClient.invoke(input, TIMEOUT_MS); // 占位:替换为实际调用
        result = parser.parse(body);                           // 占位:替换为实际解析
        if (result == null || !result.isStructurallyValid()) {
            source = "fallback";
            errorType = "PARSE_INVALID";
            result = Decision.fallback("PARSE_INVALID");
        }
    } catch (TimeoutException e) {
        source = "fallback";
        errorType = "TIMEOUT";
        result = Decision.fallback("TIMEOUT");
    } catch (Exception e) {
        source = "fallback";
        errorType = "CALL_FAILED";
        result = Decision.fallback("CALL_FAILED");
    } finally {
        log.info("openjev_call trace_id={} source={} error_type={} latency_ms={}",
                traceId, source, errorType, System.currentTimeMillis() - start);
    }
    return result;
}

日志字段至少保留四个:trace_id(串联上下游)、source(model 还是 fallback)、error_type(区分超时和格式问题)、latency_ms(判断耗时是否贴近超时上限)。fallback 里不要吞掉原始异常信息,但也不要整段打印返回体,避免把大段文本写进日志。

构造一次异常请求验证兜底是否生效

兜底路径不主动制造异常是验证不出来的。可以在测试环境用下面几种方式制造:

  1. 把配置里的 endpoint 临时指向本地一个只接受连接、不返回内容的端口,触发读取超时。可以用 nc 起一个空响应端口,具体参数受版本影响,先在本机确认再加到脚本里。
  2. 让 stub 返回 200 但 body 为空,或者返回被截断的 JSON,用来验证解析失败分支。
  3. 返回合法 JSON 但删掉 decision 字段,或把 confidence 改成字符串,验证缺失值和类型不符的处理。
  4. 直接让端口不监听,或者临时用出方向规则丢弃到目标端口的包,验证连接失败分支。用完记得删掉规则,别留在机器上。

观察点有四个:调用是否在 total-timeout-ms 附近返回,而不是一直等待;业务侧兜底动作是否真的执行了(比如进人工队列、返回默认决策);日志里是否出现 source=fallback 且 error_type 与构造的异常类型一致;主流程是否照常返回可识别的降级结果,而不是把异常抛给前端。

这几步建议在测试环境或用配置开关控制,不要直接在生产改入口地址。验证完把配置改回去,再跑一次正常请求确认 source=model。

在日志里留下决策来源标记

事后复盘时,最常见的困惑是“这条记录到底是谁给的结论”。在调用结束的日志里固定写上来源标记,问题就好定位了:

{"ts":"<ISO8601>","level":"INFO","event":"openjev_call",
 "trace_id":"<trace_id>","request_id":"<request_id>",
 "source":"model","decision":"review","error_type":"none",
 "latency_ms":0,"fallback_action":"none"}
  • source:model 表示结果来自模型,fallback 表示走了兜底。
  • fallback_action:兜底时具体做了什么,比如 queue_review、return_default;正常时写 none。
  • error_type:与上一节的取值保持一致,便于按类型统计条数。

检索方式按日志落盘格式选:纯文本日志用 grep '"source":"fallback"' app.log,JSON 日志用 jq -c 'select(.event=="openjev_call" and .source=="fallback")' app.log。要追单次请求,用 trace_id 把网关日志、调用日志和业务日志串起来,就能看出这条决策是模型给的还是兜底给的、兜底时具体落了哪个动作。

需要留意的是,source 字段只在调用封装里写一次,不要在业务各处重复记录,否则同一个 trace 下会出现多条不一致的来源标记,反而增加排查成本。