Jev 不再生成文本:用类型化概率直接驱动业务决策

2026-10-01 33 预计阅读时间: 1 分钟
来源: infoq.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

大多数大模型擅长“说出答案”,但业务系统真正需要的往往不是一段自然语言,而是一个可验证、可比较、可直接进入程序分支的决策。TypeSafe AI 推出的 Jev 将重点放在后一种场景:它不以文本生成为核心,而是返回类型化结果、概率分数和置信度,并可并行评估输入。

这条路线值得后端和平台工程师关注。它试图把模型从“需要解析的聊天机器人”变成“带不确定性的决策组件”。据发布信息,Jev 已被集成到 Vercel、Netlify 等平台,反映出这类接口在自动化工作流中的吸引力。

从生成答案转向选择答案

传统生成模型用于分类或路由时,常见做法是要求它输出 JSON:

{
  "category": "billing",
  "confidence": 0.91
}

问题在于,底层模型仍然在生成文本。它可能添加 Markdown、解释性前缀,甚至返回未定义的类别。调用方不得不增加 JSON 修复、Schema 校验、重试和兜底逻辑。

Jev 所代表的 decision-only 模式改变了接口边界。调用方预先声明允许的输出,例如:

  • approve | reject | review
  • fraud | legitimate
  • sales | support | billing

模型返回其中一个类型化决策,同时给出各候选项的概率以及置信度。业务代码不必从段落中提取答案,可以直接执行分支:

输入事件
  └─ 决策模型
       ├─ decision: "review"
       ├─ probabilities: { approve: 0.12, reject: 0.21, review: 0.67 }
       └─ confidence: 0.74

这种设计特别适合内容审核、工单路由、风控预筛、功能开关和部署策略选择。它不适合需要解释、创作、摘要或多轮对话的任务;这些场景仍然需要生成式模型。

概率不是装饰,而是控制流的一部分

只返回一个标签,会掩盖模型的不确定性。完整的概率分布允许系统区分两类表面相同的结果:

  • approve: 0.95:可以考虑自动执行。
  • approve: 0.43, review: 0.41:即使第一名仍是 approve,也应进入人工复核。

除了最高概率,还可以计算第一名与第二名之间的差值,也就是决策间隔:

margin = top_probability - second_probability

当间隔很小时,模型实际上在两个选项之间摇摆。生产系统可以同时设置置信度和间隔门槛:

confidence >= 0.80 且 margin >= 0.15  -> 自动处理
否则                                      -> 人工复核

需要注意,模型返回 0.9 并不自动意味着它在真实世界中有 90% 的正确率。团队仍需使用自己的历史数据检查概率校准,并按语言、地区、客户类型和时间窗口分别监控。模型升级后也应重新评估阈值。

可以这样封装一个类型安全的决策客户端

发布摘要没有给出 Jev 的正式 HTTP 路径、鉴权方式或字段定义。下面是一个假设性的适配层,用于展示如何把 decision-only 模型接入 Node.js 服务;运行前需要将 JEV_ENDPOINT、请求体和响应字段替换为实际接口定义。

创建项目并安装 TypeScript 运行器:

mkdir jev-decision-demo
cd jev-decision-demo
npm init -y
npm install --save-dev typescript tsx

新建 decision.ts:

type Route = "sales" | "support" | "billing";

type Decision<T extends string> = {
  decision: T;
  probabilities: Record<T, number>;
  confidence: number;
};

const routes = ["sales", "support", "billing"] as const;

function validateDecision(value: unknown): Decision<Route> {
  if (!value || typeof value !== "object") {
    throw new Error("Decision response must be an object");
  }

  const result = value as Partial<Decision<Route>>;

  if (!routes.includes(result.decision as Route)) {
    throw new Error(`Unexpected decision: ${String(result.decision)}`);
  }

  if (
    typeof result.confidence !== "number" ||
    result.confidence < 0 ||
    result.confidence > 1
  ) {
    throw new Error("Confidence must be between 0 and 1");
  }

  if (!result.probabilities) {
    throw new Error("Missing probabilities");
  }

  for (const route of routes) {
    const probability = result.probabilities[route];
    if (typeof probability !== "number" || probability < 0 || probability > 1) {
      throw new Error(`Invalid probability for ${route}`);
    }
  }

  return result as Decision<Route>;
}

function marginOf(probabilities: Record<Route, number>): number {
  const sorted = Object.values(probabilities).sort((a, b) => b - a);
  return sorted[0] - sorted[1];
}

async function classifyTicket(message: string): Promise<Decision<Route>> {
  const endpoint = process.env.JEV_ENDPOINT;
  const apiKey = process.env.JEV_API_KEY;

  if (!endpoint || !apiKey) {
    throw new Error("Set JEV_ENDPOINT and JEV_API_KEY before running");
  }

  const response = await fetch(endpoint, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      authorization: `Bearer ${apiKey}`,
    },
    body: JSON.stringify({
      input: message,
      output: {
        type: "enum",
        values: routes,
      },
    }),
  });

  if (!response.ok) {
    throw new Error(`Decision API failed: ${response.status}`);
  }

  return validateDecision(await response.json());
}

const result = await classifyTicket(
  "I was charged twice for the same subscription."
);
const margin = marginOf(result.probabilities);

if (result.confidence >= 0.8 && margin >= 0.15) {
  console.log(`Auto-route to: ${result.decision}`, result);
} else {
  console.log("Send to manual triage", { ...result, margin });
}

配置实际端点和密钥后运行:

export JEV_ENDPOINT="https://your-decision-endpoint.example/v1/decide"
export JEV_API_KEY="replace-with-your-key"
npx tsx decision.ts

这个适配层刻意保留了运行时校验。TypeScript 类型只能约束编译期代码,无法保证远程服务一定返回合法数据。即便供应商主打类型化输出,网络边界上的 Schema 校验、超时、重试和错误处理仍不可省略。

并行评估适合吞吐量,但要控制失败边界

Jev 强调并行评估输入。这对批量审核、日志分类和构建阶段检查很有价值,但并行并不等于无限并发。实际接入时可以设置固定并发数,并让单个输入失败不拖垮整个批次:

async function mapWithConcurrency<T, R>(
  items: T[],
  concurrency: number,
  worker: (item: T) => Promise<R>
): Promise<PromiseSettledResult<R>[]> {
  const results: PromiseSettledResult<R>[] = new Array(items.length);
  let nextIndex = 0;

  async function run(): Promise<void> {
    while (true) {
      const index = nextIndex++;
      if (index >= items.length) return;

      try {
        results[index] = {
          status: "fulfilled",
          value: await worker(items[index]),
        };
      } catch (reason) {
        results[index] = { status: "rejected", reason };
      }
    }
  }

  await Promise.all(
    Array.from(
      { length: Math.min(concurrency, items.length) },
      () => run()
    )
  );

  return results;
}

在无服务器环境中,还要结合函数执行时间、外部 API 限流和连接数设置并发上限。对高风险操作,批量结果应先落库或进入队列,再由独立执行器完成真正的退款、封禁或部署动作。

上线前检查:不要把置信度当授权

Jev 这类模型最有价值的地方,不是让模型替代所有业务逻辑,而是提供结构化、可度量的判断信号。采用时可以检查以下事项:

  • 输出集合是否封闭,并包含 unknown 或 review 这样的安全出口。
  • 是否同时记录最终决策、完整概率、置信度、模型版本和输入摘要。
  • 是否用真实业务数据验证阈值,而不是直接采用示例中的 0.8。
  • 低置信度、低决策间隔和接口失败是否都会进入明确的兜底路径。
  • 高风险动作是否要求规则校验或人工审批。
  • 模型升级后是否运行回归集,并比较概率分布漂移。

Decision-only 模型缩短了“模型输出到程序动作”之间的距离,也因此放大了错误决策的后果。类型安全能减少解析故障,概率能暴露不确定性,但真正可靠的系统还需要校准、审计、权限控制和人工兜底。


相关推荐