将 LLM 智能体装入高风险系统:确定性优先

内容管家 AI领域评论4字数 4029阅读13分25秒阅读模式

将 LLM 智能体部署到生产环境,关键不在于用更强大的模型,而在于把模型限制在单一节点,让系统其余部分都是普通、可测试的代码。以下是实现这一目标的核心设计思路与契约类型。

生产环境为何排斥"自主探索"式智能体

演示场景偏爱能循环调用工具、"自己搞定"的自主智能体。但生产环境对此深恶痛绝。一旦智能体的行为取决于模型当天走了哪条路径,系统就变得无法测试、无法审计、无法让它接触任何关键业务。在金融、医疗、基础设施等受监督&管理或高后果领域,"大概能用"根本站不住脚。

这是 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 是纯函数——相同上下文必然产生相同提案,无需模拟外部环境即可测试逻辑。
  • 安全:被越狱或存在缺陷的智能体只会产生糟糕的提案,而非糟糕的行动。影响范围止步于"护栏拦截或人工拒绝"。
  • 可组合:智能体之间互不调用,工作通过 Substrate 流转(如案例表 + 调度器),不存在隐含的副作用链需要追踪。

这一约束将"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]
 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: ... 节点图构成:8 个节点在所有能力间共享,只有少数节点与具体能力相关。新增一种能力,只需实现约 4 个节点,继承其余 8 个共享节点即可:

  1. entry:生成 decision_id,绑定租户与身份标识
  2. pre_check:验证输入、解析引用、低成本快速退出
  3. context_load:仅获取当前决策所需的上下文
  4. llm_decision:推理步骤——结构化输入、结构化输出
  5. output_guardrail:对模型输出进行脱敏与策略检查
  6. verification:确定性、与能力相关的正确性校验
  7. judge:采样式二次模型审核&查验(高风险场景)
  8. confidence_compose:从各信号组合评分
  9. routing:决定走自动、人工推荐还是人工必须审批
  10. prepare_proposal:塑造最终提案
  11. memory_write:写入智能体自身的审计记忆(非业务状态)
  12. exit:追加不可变账本条目

两个核心节点:决策与校验

整个系统的支柱只有两个节点:llm_decision 和 verification。

llm_decision 是唯一的非确定性节点——结构化输入、Schema 约束输出、不匹配时重试。它输出的内容在后续环节中一律视为不可信,直到通过校验。verification 则是确定性的、面向具体能力的代码,整个"把模型关在笼子里"的思路就建立在这层校验之上:

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
固定形态的用意在于:控制流可读(路径即图结构)、模型被锁定在一个节点内、每次决策格式统一——意味着每次都有相同的审计行、在同一位置添加检查/指标/护栏。

结构化输出:避免自由文本的陷阱

Node 4 值得单独说明,因为大量 LLM 脆弱性来源于一个选择:让模型返回自由文本再解析。自然语言天然歧义,格式在每次调用间漂移,下游代码随时可能因一句话的措辞变化而崩溃。"好的!大概是 Approve,不过也可能是 Escalate"——现在你得自己解决一个 NLU 问题来提取 Approve,而明天模型换一种说法你的正则就挂了。系统被耦合到了模型最不稳定的东西——它的行文风格。 解法枯燥但有效:约束模型输出经过验证的结构,把其他一切视为失败调用并重试。约束方式有三种,按保障强度排列:

  • Schema 引导(JSON Schema / response_format):请求匹配某 Schema 的 JSON,强约束,由提供商强制执行,适合大多数场景
  • 工具/函数调用:模型发出带类型的工具调用,强约束,天然适合"用参数做 X"类任务,决策映射到具体动作
  • Grammar 约束解码:将 token 约束到某语法,硬保障,无法发出无效格式,适合严格/受监督&管理格式或本地模型
from pydantic import BaseModel
from typing import Literal
class Decision(BaseModel):

decision: Literal["approve", "escalate", "reject"]
 confidence: float
 reasons: list[str]
约束生成能减少格式错误的输出,但无法彻底消除。因此要闭合反馈环:验证每个响应,不匹配则重试——把验证错误反馈给模型。设置重试上限,超限后关闭式失败(fail closed)。

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) except ValidationError as e: prompt = f"{base_prompt}\n\nYour previous output was invalid: {e}. Return JSON only." raise NonConformingOutput 边界处做验证,意味着系统其余部分只见到格式良好的决策;模型是否正常行为这个脏活被封装在这一个函数里。

用一个清晰的验证问题换掉了模糊的 NLU 问题——无需解析层、跨模型/提示词切换时依然稳定、内置护栏(固定枚举不可能返回列表外的东西)、且有类型的契约可供单元测试断言。 值得大声说明的一点:结构化约束的是形式,不是正确性。 一个完全合法的 {"decision":"approve","confidence":0.99} 可能完全错误。 结构化输出消除的是解析失败模式,而非判断失败模式——这正是它为何是生产系统的基础层,位于 evals、verification 和 confidence 之下,而不是它们的替代品。

置信度 → 路由

置信度(模型信号 + 校验 + 采样评判)由专门的置信度节点组合而成;路由节点将其转化为四条路径之一,使用单一阈值,prepare_proposal 随后将结果写入提案:

def route(confidence: float, verified: bool, T: float = 0.85) -> str:

![A diagram titled "Make Your AI Agents Boring," detailing a deterministic, linear workflow for AI agents, from entry through reasoning, guardrails, human review, and record-keeping, leading to approval or rejection.](https://www.wuaishare.cn/wp-content/uploads/2026/10/imported-image-15.webp)

if not verified:

return "reject"
 if confidence >= T:

return "auto"
 if confidence >= T - 0.2:

return "hitl_recommended"
 return "hitl_required"
verified 来自 verification 节点( 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,
 model_version TEXT NOT NULL,
 prompt_version TEXT NOT NULL,
 decision JSONB NOT NULL,
 confidence REAL NOT NULL,
 routing TEXT NOT NULL,
 outcome TEXT,
 supersedes TEXT REFERENCES decision_ledger(decision_id),
 prev_hash TEXT,
 entry_hash TEXT
);
对输入做哈希而非仓储——可验证性有了, liability 没了。账本必须是只追加的:纠错通过 supersedes 实现,绝不覆盖。如果你能 UPDATE 账本,它就不是审计轨迹——收回权限。高负载下也绝不能跳过写入——这是记录之记录,不是可丢弃的遥测数据。

有界 ReAct:循环应该待的唯一地方

如果遵循了以上所有——固定图结构、模型锁定在一个节点——迟早会遇到不适配的场景:开放性问题,模型需要查点什么、理解找到的内容、可能再查点什么、然后决策。这就是 ReAct 风格工具循环的用武之地。错误不在于循环本身,而在于无界的循环。把自主权作为深思熟虑的、带护栏的例外来开放——只在那个地方。

受控循环的四条轨道

当一个能力需要循环结构时,代码通过四条硬性约束来防止模型失控:

  1. 硬性迭代上限:使用 for 而非 while not done,确保模型无法自行决定何时终止
  2. 按能力划分的工具白名单:模型只能调用该能力显式允许的工具
  3. 全程执行追踪:每一步都记录在案,确保决策可回放
  4. 与固定流程一致的出口:循环输出仍需经过防护栏、验证、置信度评估和路由,且只提案、不行动

核心实现代码如下:

class DisallowedTool(Exception): ...
ALLOWED_TOOLS = {
 "enrich_request": {"search_kb", "fetch_record", "lookup_reference"},
}
@dataclass
class Step:
 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):
 action = deps.model.next_action(state, tools=sorted(allow))
 if action.is_final:
 return finalize(action.proposal, trace)
 if action.tool not in allow:
 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)
 return escalate("hit step cap", trace)
追踪记录会写入账本条目,因此基于循环的决策与固定图谱决策一样可重建。两条关键约束各有对应测试:

def test_always_terminates:

deps = fake_deps(model=never_final) p = bounded_react(state, deps, "enrich_request", MAX_STEPS=3) assert p.routing == "hitl_required" def test_disallowed_tool_refused:

deps = fake_deps(model=calls("danger_tool")) with pytest.raises(DisallowedTool):

bounded_react(state, deps, "enrich_request")

用还是不用:决策判断规则

判断一个能力是否需要循环结构,关键在于:达到决策所需的步骤数量和顺序,是否依赖于执行过程中发现的内容?

  • 否 → 固定图谱(适用于大多数能力)
  • 是 → 有界循环 + 四条轨道 + 明确文档记录

如果出于"安全起见"或"灵活性"而选择循环,应立即停止——这种情况下通常需要的其实是伪装成循环的固定图谱。不必要的灵活性就是在凌晨两点需要调试的不确定性。

反模式清单

  • 业务状态直接写入:模型"顺手"修改了业务状态,导致不再纯粹、无法测试,Bug 从提案问题升级为生产事故。让底层设施成为唯一的变更者。
  • 隐式状态通过临时元组或字典传递:无法对单个节点进行隔离测试,应将状态定义为类型化对象。
  • Prompt 要求"返回 JSON"但无 Schema 约束:模型仍会输出自然语言、代码块或附加说明。应使用 Schema / 工具 / 语法约束,验证后重试——约束不等于保证。
  • 直接使用模型输出的"置信度":模型置信度校准不良,应从独立信号组合计算。
  • 可变更或被跳过的审计日志:如果账本可被更新,就不是审计轨迹;在负载下跳过写入则没有记录可言。
  • 无界循环追求"灵活性":默认选择固定图谱。确需循环时,使用 for 限制步数并配置按能力的工具白名单——绝不使用 while not done 或"所有工具可用"。

核心结论

第一级(一贯应用的一种规范)包含以下要点:

  • 将模型限制在单一节点
  • 将 Agent 设计为纯函数,只生成提案
  • 用经过验证的 Schema 约束节点输出
  • 从独立信号组合置信度,将模棱两可的情况路由给人工
  • 审批后由简洁底层设施执行唯一的状态变更
  • 每项决策写入只增账本

当某个能力确实需要自主性时,予以预算——步数上限、工具白名单、全程追踪,以及与其他能力一致的输出检查。

模型仍在做它真正擅长的事——处理混乱输入时的判断。但整个系统本身是确定的、可测试的、可审计的。说到底,最好的"无聊"就是:足够可预测才能测试,足够封闭才能信任,足够可辩护才能上线。这也是本系列后续所有内容建立的基础。

系列:LLM 系统生产运行指南 —— 六级体系之一级:确定性。

 
内容管家

发表评论