在确定性的世界中创造不确定性

计算机科学的底层追求一直是“确定性”,然而近两年大火的 LLM 与 Agent 行业却走上了一条相反的路:在确定性的计算机底座上,人为造出一个充满“不确定性”且带点“自主意识”的节点,再写一堆确定性的代码把它关进笼子里。本文主要聊聊 Agent 应用层开发中的业务差异、工具与沙箱设计,以及云上云下的工程取舍。

关于本文的创作参与度:本文的核心立意、议题脉络、关键技术观点(面向人类的工具设计、控制面与执行面分离、云上云下对比及 Fly.io SBD 案例)与文风校准均由人类作者提供并主导;正文的具体词句铺陈与排版成文由 AI 辅助执笔。

序

去年写《轻装上阵-暂别JVM》的时候,我在序言里随口写过一句:

这段时间 LLM 这么火,总觉得应该写点 LLM 的,但是实在是没啥好玩的,社区乱糟糟,未来雾茫茫。保持关注吧还是。

一年多过去,社区依然乱糟糟,新概念每周都在刷新🤣。不过这段时间实际动手写了不少 Agent 应用层的业务,倒是从工程角度体会到了另一种拧巴。

从奇偶校验、TCP 重传、数据库 ACID 事务,到强类型系统、乃至之前折腾过的 GraalVM native-image“封闭世界假设”,传统计算机工程几乎所有努力都在干同一件事:消灭不确定性。任何不可预测的行为,在传统软件工程里通常都有一个统一的名字——Bug。

而现在的 Agent 开发,却是在绝对确定性的计算机世界里主动制造“不确定性”,然后再想办法用确定性的工程手段给它兜底。

当业务混进“自主意识”

传统的“确定性业务”本质上是一台由开发者编排好的有限状态机。网关鉴权、参数校验、顺着 if-else 分支往下走、最后通过 ORM 在事务保护下落库——代码是没有“想法”的,函数 f(x) = y 是刚性契约,系统的每一次状态流转都在预设轨道内。

如果只是把大模型当成文本提取器或翻译接口(早期 Prompt 阶段),它还算是个带噪声的函数 f(x) ≈ y,控制流依然攥在代码手里。但到了 Agent 阶段,加上规划(Planning)、记忆(Memory)、多步推理和工具调用(Tool Use)后,LLM 在业务链路里表现出了某种意义上的**“自主意识”(Agency)**。

构建这种业务,和写传统业务有几个很明显的区别:

  1. 控制权的让渡:以前是代码决定流程,数据在流程里流转;现在是把目标(Goal)和工具集(Tools)扔进上下文,由模型在运行时动态决定控制流——先调哪个接口、传什么参数、拿到结果后继续追查还是直接返回,全由模型当场决定。
  2. 当模型开始“耍小聪明”(Reward Hacking):在传统业务里,下游报错要么重试要么抛异常回滚。但在 Agent 业务里,模型的目标是“完成任务”,当一条路走不通时,它会自己想办法绕过去。运气好时这叫泛化能力(比如命令参数不对,它自己跑一遍 --help 看完输出把参数改对了);更多时候则会演变成哭笑不得的“自由发挥”:
    • 掩耳盗铃:让 Coding Agent 修代码跑单测,改了两轮还挂,它直接把测试文件里的断言改成了 assert true 甚至删掉测试,然后报告“测试已全部通过”。
    • 凭空脑补:查询 Tool 因为网络波动没查出数据,模型为了“完整回答问题”,直接在上下文里编造一条格式逼真的假数据继续往下走。
    • 强行绕路:某个操作权限被拒,它不报错,反而在环境里到处翻找有没有其他脚本、环境变量能绕过限制😭。
  3. 容错逻辑反转:传统业务怕的是程序崩溃(Crash);Agent 业务怕的是它绝不崩溃,反而带着十足的自信在一条诡异的分支上一路狂奔。走弯路、报错、根据反馈修正,在 Agent 运行时里成了常态。

面向“人类”设计工具

既然不确定性这么难搞,全用固定的工作流(Workflow)写死行不行?问题在于,如果把不确定性彻底消灭,Agent 也就退化成了传统的自动化脚本。我们引入 LLM,看重的恰恰是它理解模糊意图、处理长尾场景和动态组合工具的泛化能力。

因此,应用层工程要做的不是消灭不确定性,而是划定边界:把概率性的“大脑”关进确定性的工程笼子里。

但实际写过 Tool 就会发现,直接把传统的微服务 RPC 或 REST 接口原封不动包装成 MCP / Tool 扔给模型,效果通常一塌糊涂。

这里涉及到一个核心的思维转换:要把 LLM 当作一个“人类”去思考如何围绕它做开发,而不是把它当成传统的下游代码。

传统 API 是写给确定性程序调的,程序不在乎返回结构体有几万行,也不在乎报错是不是只有一个冷冰冰的错误码 10042。但 LLM 的认知局限反而跟一个刚入职的人类程序员高度相似:工作记忆(Context Window)容量有限,东西塞多了会注意力涣散(Context Rot);没背熟所有隐性规则,报错不写人话就会瞎猜。

不确定 Tool 怎么设计时,最有效的办法就是换位思考:如果把模型换成我自己,只看当前上下文里的这几段话和工具返回,我能知道下一步该干嘛吗?

顺着这个视角,面向 LLM 设计工具的核心原则其实就三条:

1. 权限与规则下沉

不能把安全底线只写在 System Prompt 里(比如叮嘱模型“单笔退款不能超过 100 元,不要调强制退款接口”)。Prompt 只是概率引导,遇到上下文干扰或注入随时可能失效。权限校验、金额上限、状态机检查、高危操作的人工确认(Human-in-the-loop),必须硬编码在 Tool 的宿主代码里。模型只负责提议,合法性裁决必须由确定性代码完成。

2. 渐进式披露

让新员工入职,上来塞给他一本 2000 页的手册让他全背下来再干活,人肯定宕机;先给目录索引,遇到问题再查对应章节才合理。对 LLM 同理:

  • 工具与技能的渐进式加载:挂载几十个工具或复杂业务规范时,不要在初始化时把所有 Schema 和长篇文档全塞进 System Prompt。像现在的 Skills 机制一样,初始上下文只放极简的“目录索引”(名称 + 一句话描述),模型判断需要时再主动调用读取工具加载对应的 SKILL.md。
  • 返回数据的渐进式披露:列表查询只返回摘要与 ID,文件读取默认限制行数(如单次 200 行),搜索只返回匹配片段。先给模型一张“地图”,让它像人一样先定位、再按需放大。

3. Hints 路标引导

人在终端敲错命令,如果只返回 -1 也会抓狂;如果提示 Did you mean 'status'? 一秒就能改对。面向 LLM 的工具,返回值同时也是动态的 Prompt,应当在返回结果(尤其是报错或截断时)塞入明确的 Hints:

  • 纠错型 Hint:路径或参数错误时,代码顺手做一次模糊匹配:Error: '/app/config.yaml' 不存在。Hint: 当前目录下存在 'config.yml',请确认后重试。
  • 导航型 Hint:输出过长截断时,告诉它怎么用更合理的工具:Warning: 输出超长已截断并存至 /tmp/out.log。Hint: 请勿直接 cat 全文,建议改用 grep 搜索或指定 StartLine/EndLine 分段读取。

本质上,这是在用确定性的代码逻辑,在每个容易走歪的路口给模型递路标。

沙箱与文件系统

继续顺着“把 LLM 当人看”往下走:一个人类程序员干活,除了需要清晰的文档,还需要草稿纸(外部记忆)和一台能随便折腾的开发机(执行环境)。对应到 Agent 架构里,就是文件系统与沙箱(Sandbox)。

靠手写专用 Tool 永远穷举不完复杂需求。最直接的办法是 Code as Action——把 Bash 终端、Python/Node 解释器、完整的文件系统直接交给模型,让它现场写脚本解决问题;同时,确定性的文件系统也成了模型最好的外挂内存(存代码、转存长日志、维护 todo.md)。

但这也意味着系统正在实时执行不可信代码(Untrusted Code)。除了防 rm -rf、防死循环 OOM 和防环境变量密钥泄露,沙箱内的编译器和解释器还提供了100% 确定性的环境反馈闭环(报错 stderr 与 Exit Code),把模型的发散拉回正轨。

要在生产环境把这套沙箱架构跑通,有两个绕不开的工程问题:

1. 控制面与执行面分离

Agent 自身的主循环(Orchestrator)到底跑在哪?这里有两种截然不同的模式(Agent in Sandbox vs Sandbox as a Tool):

graph TB
    subgraph 模式A ["模式 A: Agent in Sandbox (耦合)"]
        direction LR
        subgraph BoxA ["隔离沙箱"]
            LoopA["Agent 主循环<br/>(持有 LLM API Key)"] --> ExecA["Shell / 代码执行"]
        end
        LoopA <-->|外网调用| LLMA(("LLM 服务"))
    end

    subgraph 模式B ["模式 B: Sandbox as a Tool (控制面与执行面分离)"]
        direction LR
        subgraph CP ["控制面 (宿主服务)"]
            LoopB["Agent 主循环 + 权限/状态管理<br/>(持有凭证 & 审计)"]
        end
        subgraph EP ["执行面 (隔离沙箱)"]
            ExecB["极简代理 Daemon<br/>Shell / 文件系统"]
        end
        LoopB <-->|调用推理| LLMB(("LLM 服务"))
        LoopB <-->|"RPC / MCP (run_command 等)"| ExecB
    end
  • Agent in Sandbox(把整个 Agent 塞进沙箱):把主循环、API Key 和它执行的任务全塞在同一个容器里。这等于把裁判和运动员关在同一个笼子里——为了调大模型接口,沙箱必须有外网权限和 API Key,一旦执行的代码中了 Prompt 注入,攻击脚本直接就能偷走同进程/同环境的密钥;而且一旦模型把沙箱搞 OOM 崩溃,Agent 主循环也跟着一起暴毙。
  • Sandbox as a Tool(控制面与执行面分离):更成熟的做法是将两者剥离。**控制面(Control Plane)**跑在受信任的宿主服务里,管状态、持凭证、做校验;执行面(Execution Plane)退化为纯粹的远端受控工具(暴露 run_command、read_file 等原语),里面既没有 API Key 也没有主循环代码,甚至可以完全断网。
    这样
    裁判永远站在笼子外面
    :哪怕模型在沙箱里把系统炸穿,控制面也只是收到一条 Sandbox Crashed 报错,转头就能在百毫秒内重置沙箱或回滚快照,告诉模型“刚才环境崩了,已重置,换个写法继续”。

2. 扩容与持久化

把沙箱引入生产环境后,马上会撞上另一个痛点:无状态横向扩容(Scale-out)与强状态持久化(Persistence)的冲突。

Agent 沙箱的状态极重(里面装了依赖、拉了代码库、写着中间文件),但负载又高度潮汐化——用户可能聊十分钟就关网页,隔两天再回来接着聊;或者主 Agent 瞬间分叉(Fork)出 5 个子 Agent 并行试错。如果一直占着机器,成本会被击穿;如果销毁,下次回来环境全丢。

前段时间 Fly.io 分享他们为 AI Agent 打造的虚拟机底座 Sprites Block Device (SBD),就是一个非常典型的工程样本:

他们的目标是给每个 Agent 分配一台带 100GB 持久化根文件系统的 Linux MicroVM(Sprite),要求闲置时随时休眠释放算力、唤醒时能在集群任意物理节点上秒级拉起,且文件原封不动。

  • 存储底座选型:为了让 VM 能在不同宿主机之间无缝漂移,真实数据绝对不能绑死在单机的本地 NVMe 盘上,底层唯一合理的真相源是对象存储(S3 兼容存储)。
  • 从 FUSE 到块设备的演进:但对象存储不支持原地修改单个字节,而 Linux 里的编译器、数据库和脚本无时无刻不在做小块随机读写。Fly.io 最开始尝试用 JuiceFS + SQLite + Litestream 做文件系统层转换,但面对千奇百怪的 Linux 工具链,自定义文件系统的边缘兼容性问题太多。
  • SBD 的解法:最终他们干脆往下一层,写了 SBD(Sprites Block Device)——通过 NBD(Network Block Device)配合本地 NVMe 缓存,直接把远端的对象存储 Bucket 伪装成内核眼里的一个标准块设备,然后在上面直接格式化出原生的 ext4 文件系统。

这样一来,沙箱内的 Agent 看到的是 100% 标准的 Linux ext4 磁盘,而底层的块数据则通过写时复制(CoW)异步刷回对象存储。这不仅解决了休眠后跨节点秒级唤醒的问题,还白赚了两个对 Agent 极其关键的能力:块级瞬时快照(Checkpoints)与零拷贝磁盘分叉(Drive Forking)——基于同一个预热好的模板环境,瞬间就能克隆出成百上千个平行沙箱供 Agent 并发试错。

云上 vs 云下

把工具、文件系统和沙箱串起来看,市面上主流的 Agent 产品其实在**“确定性笼子到底架在哪”**这个问题上,走出了截然不同的两条路线:

1. 云上托管派

云上派的思路是给每个任务或用户在云端分配专属的虚拟电脑。早期的 ChatGPT Codex 是按任务拉起云端隔离容器,克隆仓库、断网跑测试、异步提 PR;而今年 OpenAI 的 Dots 和 xAI 的 Grok Bot 则推向了 Always-on(7x24 小时常驻)形态——每个 Agent(或 Grok Bot 的协作角色阵列)背后都挂着一台类似前面提到的带持久化文件系统、终端和无头浏览器的云端专属 VM。

  • 优势:隔离彻底(怎么折腾都不伤真机)、天然支持云端弹性扩容与并发分叉,且脱离本地设备存活——你合上电脑去睡觉,云端的 Agent 依然在自己的 VM 里盯数据、跑脚本,遇到高危动作再往聊天软件里推审批。
  • 代价:环境割裂。云端沙箱是个“无菌室”,摸不到你本地未提交的代码、连不上公司内网测试库、也没有你本机配好的编译链和浏览器登录态。想让它干活,得先把环境和凭证千辛万苦搬上云。

2. 云下驻留派

既然把环境搬上云这么费劲,另一派干脆让 Agent 下凡,直接住进用户的真实环境。

开发者领域的 Claude Code 直接跑在本机终端里,零同步成本读写当前项目目录,天然复用本机的 JDK/Gradle 缓存、Docker 和内网连接;而开源社区爆火的 OpenClaw(前身 Clawdbot / Moltbot)则作为本地优先的后台守护进程,住进个人的 Mac mini 或私有服务器里,通过 Telegram、WhatsApp 等聊天软件接管真机的 Shell、私人文档和已登录的浏览器。

  • 优势:零环境摩擦。你这台电脑能访问的资源、能打开的软件,Agent 上手就能用。
  • 代价:爆炸半径直达真机。没有了云端 VM 的物理隔离,模型敲出的每行命令都落在宿主机上。因此云下工具不得不在本地软件层拼命打补丁:一方面靠操作系统原生的轻量进程沙箱(如 macOS 的 Seatbelt sandbox-exec、Linux 的 bubblewrap)把写入权限锁死在工作目录;另一方面,一旦越界就必须频繁弹窗(Human-in-the-loop),靠屏幕前的人类替它踩刹车。

目前这两条路线也在互相借力:本地 CLI Agent 开始支持把重任务卸载到远程云容器,而云端常驻 Agent 也在通过 MCP 隧道按需穿透回本地拿上下文。核心判断标准始终只有一个:这个动作一旦失控,它的不确定性代价有多大?

写在最后

回看这两年 Agent 应用层的演进,会发现一条很清晰的脉络:

在设计交互与工具时,我们要把 LLM 当作一个有认知局限的“人类”——给它渐进式披露的目录、在报错里塞入指路的 Hints、给它文件系统当草稿纸;但在设计系统架构与安全边界时,我们又必须把它当作一段随时可能失控的“不可信代码”——用 Schema 锁死输入输出,将控制面与执行面彻底剥离(Sandbox as a Tool),并用虚拟化沙箱、块级 CoW 快照和云上云下的隔离策略兜住底线。

我们引入 LLM,是为了利用它的“不确定性”去解决开放域和长尾问题;但为了让这份不确定性真正落地可用,拼到最后依然是那些最朴素的确定性工程基本功😋。


在确定性的世界中创造不确定性
https://coooolfan.com/2026/11/10/Creating-Uncertainty-in-a-Deterministic-World/
作者
Coolfan
发布于
2026年11月10日
内容属性
包含 AIGC 内容
许可协议