前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
返回 AI 情报前线
All News · 全部资讯9492
  • GPT-6发布:内置可交互UI组件,Haiku 5.5降价75%
  • Claude Haiku 5.5 定价解析:超 10 万 token 后性价比骤降
  • Agent 置信度层:何时判定模型「不知道」
  • Claude Haiku 5.5 正式发布:百万上下文、百万 token 仅 $0.10
  • 让 AI Agent 变得无聊:确定性层设计
  • GitHub Copilot 已上线 Claude Haiku 5.5
  • DeepGEMM 开源:英伟达 GPU 高性能 GEMM 内核库
  • 如何防止AI Agent向诈骗者付款:地址风险检查实战
  • x402支付第四大陷阱:无支出策略导致的钱包耗尽
  • GPT-6 发布:ChatGPT 新增交互式 UI,可生成图表按钮
  • Claude Haiku 5.5 登陆 AWS,性价比提升 75%
  • Claude Haiku 5.5 基准跃升 72.4%,价格下调九成
  • n8n AI Agent 五大生产环境故障及修复方案
  • 验证Agent不要看输出,要看状态diff
  • Claude Code按决策类型而非难度分配模型
  • K3 vs DeepSeek V4:开源模型如何选型
  • OpenAI 发布 722 篇数学论文
  • Cloud Run 免费自定义子域名:告别复杂 Demo URL
  • OpenAI 失控 Agent 在维基媒体项目活动曝光
  • GitHub Copilot 修复用量统计中 Agent 活动丢失问题
  • OpenAI 发布数百个数学难题的解答成果
  • 29款LLM谄媚度基准测试:前沿模型稳住了,小模型全面溃败
  • llm-openai-decisions 插件发布:支持 GPT-6 Astra 决策 API
  • Anthropic开放Claude最强版本用于安全测试:已发现10万漏洞
  • 谷歌 EmbeddingGemma 2:7.4 亿参数多模态嵌入模型,手机端 191MB 即可运行
  • 无 API 桌面应用远程控制方案:用 CDP 桥接手机与 Claude Code
  • AI功能按调用计费:多数设计在浪费预算
  • Simon Willison 点评 EmbeddingGemma 2:开源权重是嵌入模型唯一理智选择
  • Mistral Large 4预览发布:参数破万亿
  • Google发布EmbeddingGemma 2:开源多模态嵌入模型
  • Google 开源 EmbeddingGemma 2:740M 参数端侧向量模型,191MB 内存跑离线 RAG
  • 间接提示注入攻击链解析与防御架构
  • Whisper 口述转文字 WER 从 8.5% 降至 2.5% 的实战总结
  • 用 AgentCore + OpenClaw 构建持久记忆的个人 AI 助手
  • Google开源EmbeddingGemma 2:7.4亿参数多模态向量模型
  • 已加载 35 / 9492
8.0
热点
AI SCORE
技术实践2026-10-08 04:14

让 AI Agent 变得无聊:确定性层设计

Stack Overflow Blog#Agent#架构#确定性
Editor brief · 编辑速览

Stack Overflow 博客第一篇,介绍如何通过确定性层让 Agent 行为可预测、可复现,避免随机性导致的调试困难,是生产级 Agent 架构核心模式。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

第一部分:让你的 AI 智能体变得无聊:确定性层

将 LLM 智能体放入高风险系统的诀窍不在于更聪明的模型——而在于将模型限制在单一节点,从而让系统其余部分都是普通的、可测试的代码。以下是实现这一目标的结构性手法,以及相应的契约和类型定义。

Demo 喜欢那种循环、调用工具、"自己搞定"的自主智能体。生产环境厌恶它们。一旦智能体的行为取决于模型今天游走到了哪条路径,你就无法测试它、无法审计它、也无法让它触碰任何关键的东西。在受监管或后果严重的系统——资金流转、医疗健康、基础设施——"它通常能工作"根本行不通。

这是将 LLM 系统投入生产运营的六级成熟度模型的第一级:确定性层。在你能够做评估、置信度路由或任何更高层级的操作之前,你需要底层的东西先稳住。这一层的全部精髓就是一个想法——将模型限制在单一节点,让周围的一切都是普通的、可测试的代码——并通过一些设计手法来表达它。你保留了模型的智能,但去掉了几乎所有不可预测性。这篇文章给出的是契约,而不仅仅是概念。

智能体提议;它们不执行

最重要的规则:智能体是从上下文到提议决策的纯函数——就业务状态而言是纯的,在给定网关的情况下是确定性的(唯一的非确定性调用,即模型,是被注入的,因此测试时可以伪造它)。智能体没有改变世界的权限。一个单独的、笨重的、经过大量测试的组件——底层基座(substrate)——应用这些提议,且仅在获得所需批准之后才执行。

from typing import Protocol, Literal
from dataclasses import dataclass

@dataclass(frozen=True)
class Proposal:
    decision_id: str
    capability: str
    action: dict                  # 结构化的提议变更——尚未应用
    confidence: float
    routing: Literal["auto", "hitl_recommended", "hitl_required", "reject"]
    reasoning: list[str]
    evidence: list[dict]

class Agent(Protocol):
    def propose(self, ctx: "Context") -> Proposal: ...     # 纯函数:不产生业务状态的副作用

class Substrate(Protocol):
    def apply(self, proposal: Proposal, approval: "Approval") -> "Effect": ...  # 唯一的变更器

因为 propose() 是纯函数,它最核心的特性就是一条断言:

def test_propose_is_pure():
    agent = ClassifyAgent(gateway=FakeGateway(scripted))
    assert agent.propose(ctx) == agent.propose(ctx)   # 相同上下文 → 相同提议;未触碰世界

这个边界为你带来了三个属性:

可测试。propose() 是一个纯函数——相同的上下文,相同的提议。测试逻辑时不需要模拟整个世界。

安全。被越狱或有缺陷的智能体产生的是一个糟糕的提议,而不是糟糕的动作。爆炸半径止步于"某个护栏或某个人说了不"。

可组合。智能体永远不会调用其他智能体。工作通过底层基座流动(例如一张 cases 表加一个调度器),因此不存在需要推理的隐藏副作用链。

这一条约束将"一个 AI 做了某件我们无法解释的事"变成了"一个 AI 建议了某件事,以下是批准它的确切依据。"

每个能力对应一张固定图

自由形式的 ReAct 循环适合探索,但不适合提供保证。将每种能力建模为一个固定的节点序列,模型仅在需要判断的地方使用,其他一切都是普通代码。下面的箭头草图为了可读性做了精简;完整的节点列表(含 pre_check 和 memory_write)在下一节。

entry → load context → reason (LLM) → output guardrail → verify →
        judge (sampled) → compose confidence → route → prepare proposal → record → exit
                自由形式 ReAct 循环        固定图(本文)
--------------  -----------------------  ---------------------------------
控制流          模型决定下一步            事先已知
可测试性        困难(路径可变)          每个节点独立可测试
延迟/成本      无上限                    有上限,可预测
审计           从跟踪记录重建             每次都是统一的行
适用场景        开放式探索                必须有保证的决策

你放弃了一些巧妙的灵活性;换回来的是推理系统行为的能力。巧妙的判断部分——对混乱输入的判断——保留在模型最擅长的地方,在一个节点内,被你能读懂的代码包围。

"让智能体具有确定性"在看到具体的形态之前很难付诸行动。让我们逐一节点地走一遍单个请求在图中的完整流程——即上图背后的实现。

状态对象。每个节点读取并写入贯穿整个图的一个类型化状态值。将这一点明确化就解决了一半的问题——这正是让你能够通过构造一个状态并对结果做断言来独立测试每个节点的原因。

from typing import TypedDict, Literal, Optional

class GraphState(TypedDict):
    decision_id: str
    tenant_id: str
    identity: dict                      # 身份/权威(入口处设置)
    inputs: dict                        # 经验证的请求
    context: dict                       # 加载的记忆/引用片段
    model_output: Optional[dict]        # LLM 节点的原始结构化输出
    guardrail: dict                     # 已应用的块/脱敏处理
    verification: dict                  # 确定性检查结果
    judge: Optional[dict]               # 第二次意见结果(如有采样)
    confidence: Optional[float]         # 组合后的分数
    routing: Optional[Literal["auto", "hitl_recommended", "hitl_required", "reject"]]
    proposal: Optional[dict]            # 最终成型的提议
    ledger_entry_id: Optional[str]

节点契约。每个节点都是相同的形状:state -> state。除模型节点外都是确定性的。这种一致性正是你可以对每个节点做单元测试并推理整个系统的原因。

from typing import Protocol

class Node(Protocol):
    name: str
    def run(self, state: GraphState, deps: "Deps") -> GraphState: ...

图结构。八个节点在所有能力间共享;只有少数是能力特定的。新增一种能力 = 实现约 4 个节点,继承其他 8 个。

1  entry              生成 decision_id,绑定 tenant + identity              [共享]
2  pre_check          验证输入、解析引用、廉价的早期退出                  [能力特定]
3  context_load       仅获取此决策所需的上下文                            [共享]
4  llm_decision       推理步骤——结构化输入,结构化输出                   [能力特定]
5  output_guardrail   对模型输出进行 PII/策略清洗                        [共享]
6  verification       确定性、能力特定的正确性检查                        [能力特定]
7  judge              采样后的第二次模型审查(高风险场景)               [共享]
8  confidence_compose 从各信号组合分数                                    [共享]
9  routing            自动 vs 人工介入 vs 拒绝                            [共享]
10 prepare_proposal   塑造最终提议                                        [能力特定]
11 memory_write       写入智能体自身的审计记忆(非业务状态)              [共享]
12 exit               追加不可变的账本条目                                [共享]

两个承重节点是 llm_decision 和 verification。第一个是唯一的非确定性节点——结构化输入、schema 约束输出、 mismatch 时重试——周围的一切在其被检查之前都将输出视为不可信的(下一节详述)。第二个是确定性的、能力特定的代码,整个"将模型限制住"的主张就建立在此之上:

def run(self, state, deps):
    out = state["model_output"]
    checks = {
        "in_enum":   out["decision"] in ALLOWED_DECISIONS,        # 不能返回列表外的动作
        "schema_ok": matches_schema(out, DECISION_SCHEMA),
        "rules_ok":  deps.rules.check(out, state["inputs"]),      # 业务不变量
    }
    state["verification"] = {"checks": checks, "score": sum(checks.values()) / len(checks)}
    return state

固定形态的要点在于:你可以读取控制流(路径就是图),模型被限制在一个节点内,每个决策都是统一的——每次形状相同意味着每次审计行相同,也意味着在同一个位置添加检查、指标或护栏。

结构化输出优于自由文本

节点 4 值得单独讨论,因为大量 LLM 的脆弱性来自一个选择:让模型返回自由文本然后再解析。散文是模糊的,格式在多次调用之间漂移,你的下游代码距离因一个意外措辞而崩溃只有一步之遥。"当然!大概是 Approve,虽然也可能是 Escalate"——现在你有了一个 NLU 问题要处理才能提取出 Approve,而明天的改写就会让你的正则表达式失效。你已经将系统耦合到了模型的散文风格,而这是它最不稳定的东西。

解决方案既枯燥又强大:约束模型输出经验证的结构,将其他任何输出视为失败的调用并重试。根据所需保证的严格程度,有三种约束方式:

method                                         how                             guarantee                             use when
---------------------------------------------  ------------------------------  ------------------------------------  --------------------------------------
Schema-guided (JSON Schema / response_format)  ask for JSON matching a schema  strong, provider-enforced             most cases
Tool / function call                           model emits a typed tool call   strong; natural for "do X with args"  the decision maps to an action
Grammar-constrained decoding                   constrain tokens to a grammar   hard guarantee (can't emit invalid)   strict/regulated formats, local models
# the contract as types — downstream consumes a typed object, never prose
from pydantic import BaseModel
from typing import Literal

class Decision(BaseModel):
    decision: Literal["approve", "escalate", "reject"]   # an enum is itself a guardrail
    confidence: float
    reasons: list[str]

约束生成减少了格式不良的输出,但没有完全消除它。所以要闭合这个循环:验证每个响应,在不匹配时重试——将验证错误反馈回去。限制重试次数,失败时直接拒绝。

from pydantic import ValidationError
class NonConformingOutput(Exception): ...

def decide(base_prompt, model, max_retries=2) -> Decision:
    prompt = base_prompt
    for _ in range(max_retries + 1):
        raw = model.generate(prompt, schema=Decision.model_json_schema())
        try:
            return Decision.model_validate_json(raw)            # success: typed object
        except ValidationError as e:
            # rebuild from base_prompt — don't append onto the already-appended prompt (it compounds)
            prompt = f"{base_prompt}\n\nYour previous output was invalid: {e}. Return JSON only."
    raise NonConformingOutput()      # fail closed — never hand downstream a guess

边界验证意味着系统其他部分只会看到格式正确的决策;"模型是否按预期行为"这个棘手的问题被限制在这个函数中处理。你用模糊的 NLU 问题换取了清晰的验证问题——无需解析层,在模型或提示词变更时保持稳定,内置了护栏(固定的枚举无法返回列表以外的值),还获得了黄金测试可以断言的强类型契约。

需要大声强调一点:结构约束的是形式,而不是正确性。一个完全有效的 {"decision":"approve","confidence":0.99} 可能是完全错误的。结构化输出消除了解析失败模式,而不是判断失败模式——这就是为什么它是生产系统的地板,在 evals、验证和置信度之下,而不是它们的替代品。

粘合剂:置信度 → 路由

置信度(模型信号 + 验证 + 采样评判)在置信度节点被组合;路由节点将其转换为四个路径之一,使用单一阈值,prepare_proposal 随后将结果印在提案上:

def route(confidence: float, verified: bool, T: float = 0.85) -> str:  # T is per-capability config, not a constant
    if not verified:           return "reject"
    if confidence >= T:        return "auto"
    if confidence >= T - 0.2:  return "hitl_recommended"   # close: pre-fill the proposal for a human
    return "hitl_required"                                 # low: a human decides from scratch

verified 来自验证节点(verified = all(checks.values())),而不是智能体自己设置的字段,路由器发出所有四种路由状态。注意顺序:路由在提案被塑造之前运行(节点 9 → 节点 10),所以它接收的是普通置信度和 verified 标志——而不是 Proposal。将 T 设得保守——一切都由人工处理——只有当数据证明安全时才按切片降低它。人类注意力,这个昂贵的资源,被精确地花在系统不确定的地方。

分类账:仅追加的记录之记录

每个提议的决策都变成一行不可变的记录——无条件地作为每个决策的最后一个节点写入。不是日志行;而是关于发生了什么以及为什么发生的规范记录。

CREATE TABLE decision_ledger (
  decision_id     TEXT PRIMARY KEY,
  ts              TIMESTAMPTZ NOT NULL,
  tenant_id       TEXT NOT NULL,
  capability      TEXT NOT NULL,
  inputs_hash     TEXT NOT NULL,          -- hash, not the raw sensitive payload
  model_version   TEXT NOT NULL,
  prompt_version  TEXT NOT NULL,
  decision        JSONB NOT NULL,
  confidence      REAL NOT NULL,
  routing         TEXT NOT NULL,          -- auto | hitl_* | reject
  outcome         TEXT,                    -- recorded later as a NEW superseding row, never an in-place UPDATE
  supersedes      TEXT REFERENCES decision_ledger(decision_id),
  prev_hash       TEXT,                    -- optional hash-chain for tamper-evidence
  entry_hash      TEXT
);
-- append-only: no UPDATE/DELETE grants; corrections are new rows that set `supersedes`.

对输入进行哈希处理,而不是存储它们——在不承担责任的情况下实现可验证性。使其仅追加:更正取代,永不覆盖。如果你可以 UPDATE 分类账,那它就不是审计跟踪;撤销该授权。在高负载下也不要跳过写入——它是记录的记录,不是可丢弃的遥测数据。

有界 ReAct:循环属于的唯一位置

如果你遵循了以上所有——固定图、模型被限制在一个节点——你最终会遇到一个不合适的问题:开放式的任务,模型必须查找某些内容、推理它发现的内容、可能再查找更多、然后决策。这就是 ReAct 风格工具循环的用途。错误不是循环本身;而是无界循环。将自主性作为一种深思熟虑的、有护栏保护的例外来允许——而不是在其他任何地方。

四条护栏保持控制:(1) 硬性迭代上限——用 for,而不是 while(not done),这样模型不会决定何时停止;(2) 按能力的工具允许列表——它只能调用显式限定在此能力范围内的工具;(3) 完整的每次迭代跟踪——每一步都被记录,以便决策可以重放;(4) 与其他所有流程相同的出口——循环的输出仍然通过护栏、验证、置信度和路由,最后仍然是提议而不是行动。

class DisallowedTool(Exception): ...    # raised when the loop reaches for an off-list tool

ALLOWED_TOOLS = {                       # rail 2: per-capability allow-list, not "all tools"
    "enrich_request": {"search_kb", "fetch_record", "lookup_reference"},
}

@dataclass
class Step:                              # rail 3: one trace row per iteration
    i: int; action: str; args: dict; result_digest: str

def bounded_react(state, deps, capability, MAX_STEPS=6) -> Proposal:
    allow = ALLOWED_TOOLS[capability]
    trace: list[Step] = []
    for i in range(MAX_STEPS):                                   # rail 1: hard cap
        action = deps.model.next_action(state, tools=sorted(allow))
        if action.is_final:                                     # check FIRST — a final answer carries no tool
            return finalize(action.proposal, trace)             # rail 4: still a PROPOSAL
        if action.tool not in allow:                            # defense in depth
            raise DisallowedTool(action.tool)
        result = deps.tools[action.tool](**action.args)
        trace.append(Step(i, action.tool, action.args, digest(result)))
        state = state.with_observation(result)                  # loop-local state type, not the fixed-graph GraphState
    return escalate("hit step cap", trace)                      # bounded: cap hit → returns a Proposal routed "hitl_required"

跟踪记录进入分类账条目,因此基于循环的决策与固定图决策一样可重建。两条护栏各值得测试——它总是终止,并且无法调用允许列表之外的工具:

def test_always_terminates():
    deps = fake_deps(model=never_final)                 # a model that never returns is_final
    p = bounded_react(state, deps, "enrich_request", MAX_STEPS=3)
    assert p.routing == "hitl_required"                 # hit the cap → routed to a human, didn't hang

def test_disallowed_tool_refused():
    deps = fake_deps(model=calls("danger_tool"))        # a tool not on the allow-list
    with pytest.raises(DisallowedTool):
        bounded_react(state, deps, "enrich_request")

关于是否需要这一层的决策规则:到达决策是否需要依赖发现过程中的步骤数量和顺序?如果否 → 固定图(大多数能力场景)。如果是 → 有界循环、四轨、记录为例外。如果你"为了安全"或"为了灵活性"而使用循环,停下来——那通常是固定图穿了马甲。不需要的灵活性就是凌晨两点还得调试的非确定性。

智能体"就这一次"地写入业务状态。现在它既不纯粹、也无法测试,bug 从一个坏提案变成了一次事故。让底层设施成为唯一的变更者。

在步骤之间以临时元组/字典形式传递的隐式状态——你会失去孤立测试节点的能力。让状态成为一个类型化对象。

在 prompt 中写"返回 JSON"但不强制 schema——你仍然会收到自然语言回复、代码围栏或尾部注释。使用 schema/工具/语法强制,然后验证并重试;受约束 ≠ 有保证。

从模型中"借用"的"置信度"。校准失准;改为从独立信号组合。

可变或被跳过的审计。如果你能 UPDATE 账本,它就不是审计轨迹;如果在负载下丢弃写操作,你就失去了记录本身。

无界循环"为了灵活性"。默认为固定图。当你确实需要循环时,使用 for 上限和每能力的允许列表——永远不要用 while not done 或"所有工具可用"。

第一级是一种纪律的一致应用:将模型限制在一个节点,让智能体成为一个只做提议的纯函数,将该节点约束为经过验证的 schema,从独立信号组合置信度以将边界情况路由给人类,让一个朴素的底层设施在批准后成为唯一变更者,并将每个决策写入只追加账本。当某个能力真正需要自主性时,给它做预算——一个步骤上限、一个工具允许列表、一条追踪,以及与其他所有内容相同的输出检查。

模型仍然在做它唯一擅长的事——对混乱输入的判断——但它周围的系统是确定的、可测试的、可审计的。无聊,是最好的那种无聊:可预测到足以测试,受控到足以信任,有据可查到足以发布。这就是本系列所有后续内容建立的基础。

系列文章:生产环境运行 LLM 系统 — 第 1 级(共 6 级):确定性。

Original source

本文由 AI 翻译整理自 Stack Overflow Blog,原文版权归原作者所有。

阅读英文原文
上一篇
Claude Haiku 5.5 正式发布:百万上下文、百万 token 仅 $0.10
下一篇
GitHub Copilot 已上线 Claude Haiku 5.5