上下文工程的另一半:把系统重写成 AI 可读的形式语言

上个月业务方报了个问题:页面上一个组件,该隐藏的时候没隐藏。

查起来不费事。条件写在 visibleOn 里:"eligible === true"。字段是协议认的,表达式本身挑不出病;病在它落脚的位置------eligible 是行数据里的字段,这行配置却挂在组件上;和函数外访问不到局部变量一个道理,求值时行数据不在场。条件算出 undefined,这个位置的缺省是判不了就放行,组件于是永远显示。全程没有报错。

引擎没拦,因为在它那里这不算错。同一个「满足条件才显示」的意图,协议里有八种拼法,visibleOnhiddenOnwhen 各算一种;core 甚至按字段名后缀自动认领,写 fooOn 都算合法 API。至于一行条件会在哪个上下文里求值、能引用什么,schema 里一个字都没有。这一行和写对的那些放在一起,分不出来。

定位花了一下午;回查才发现,这行 schema 已经在线上躺了三个星期,落地的时候没有发出任何声音。补一条规则说「别用 visibleOn」堵不上:拼法收不尽,后缀认领还在发明新的;而且病根不在名字:写下这一行的时候,没人能只看这一行,就知道自己在哪个上下文、能引用什么、写错了会不会响。

位置:上下文工程的另一半

这一年多,行业在忙一件事:把模型喂好。Karpathy------前特斯拉 AI 总监------把「上下文工程」(context engineering)带成了显学。Anthropic 的官方指南教人算注意力预算、做压缩、把笔记写到窗口外;OpenAI 更早,2023 年就在讲 scaffold,给模型搭脚手架。

这些全是供给端的事:喂什么、怎么管、怎么搭。开头那三个星期的沉默,不在这三样里。

系统外围这一圈,也有人了。仓库门口立着 AGENTS.md,告诉编码代理这里的规矩;MCP 管的是系统和 AI 之间怎么连。一个管喂,一个管连。连上、喂好之后,还剩一层没人管:模型能不能看懂系统内部的语义。这一层还没有名字。

我们做的就是这一层。开头那份 schema,是按我们低代码引擎的协议写的。过去一年多,行业在补供给端,我们把引擎的协议重写了一遍。引擎没换,换的是写法。假设只有一条:写入者没有记忆,每次来都是新人。

写法按用途分成一类一类:状态值、快捷键、条件写法,各算一类。改完的那几类,同一个意图只剩一种写法,写错当场就响。没改完的还是老样子,条件写法这一类就排在名单上:

  • 「满足条件才显示」这个意图,今天还有八种拼法;
  • 一行条件在哪求值、能引用什么,协议里还是一个字都没有。

开头那种写错位置、一声不吭的事故,今天还可能发生;等这一类改完,它响在写入当场,而不是躺在线上。账也简单:同一套引擎,模型的产出从逐条人工复核,变成机器先拦、人抽检。

低代码只是我们手边的例子。页面协议、工作流定义、基础设施配置------凡是这类定义被 AI 高频写入的系统,错法相同,治法也相同。

写入循环

模型没有项目记忆。每次会话都是新的:它能看到的,只有你摆进去的,和它自己翻出来的片段;看不到的,用自己的分布补全。补全的方式是猜,猜的分布不由你控制。

它的一次写入是个写入循环(write cycle):读懂眼前的片段,动手改 schema,可能出错,下次再来时面对新的现场。循环里流转的是现场,不是记忆------它一轮结束就什么都不带走。被修正的是系统,不是它:写错的 schema 当轮改对,协议随错误统计进化;进化出来的是机器执行的约束,它读不读都生效。这里先只看一个写入者。

每一步都对系统提了一个问题:

  1. 看一个片段,能懂吗------可解码性
  2. 这里能不能动------机制与策略
  3. 做错了,谁知道------报错即协议
  4. 下次来,看到什么------信号纪律

这四个问题不是新发明。计算机科学的老分支形式语言------正则表达式、编译器都是它的下游------对一门语言的要求是:语义由构造组合,语法只管机制、不管怎么用,非法句式必被拒绝,证明只准引用写下的前提。四条对着四问,一条不缺。标题里的「形式语言」,指的就是这个。

第一维 · 读懂:可解码性

第一问是「看一个片段,能懂吗」。我们的答案定成一条判据,叫局部可解码(local decodability):一个协议片段,看片段本身就能解出语义。不依赖全局状态,不依赖隐藏的运行时上下文,不需要「知道内情」。开头那行 visibleOn 就过不了这条判据------只看那一行,看不出它在哪个上下文求值。

和「代码可读性」不是一回事。人遇到歧义,可以翻文档、问同事、凭经验;模型遇到歧义,只会猜。可读性那套标准,是照着有记忆的人定的;可解码性是照着没有记忆的写入者定的。

判据的反面是一组红线,评审协议改动时逐条对:

红线 一句话例子 模型为什么会错
新造词 协议里出现项目自造的概念名 训练分布里没有,只能按字面猜
隐式分支 「某些情况下」引擎自动转换类型 触发条件不可见,行为无法预测
全局状态定语义 同一字段在两种模式下含义不同 片段本身看不出当前模式
黑箱组件 行为藏在命令式实现里 只能当魔法用,错了无法定位

决策点

红线条目背后是同一件事:决策点

协议里的「灵活」 对人 对模型
两个字段二选一,但都能出现 记住约定 一次下注
多个相似 API 并存 查一次文档 一次下注
引擎帮忙隐式转换 靠经验 一次下注
攒下来 记忆成本 错误率

协议里每处「灵活」,对人是一条要记住的约定,对模型是一次下注。单次胜率再高,生成次数一乘,累积的错误数也不会小------就算一次九成九地对,一千次生成也要押错十次。

决策点不会自己消失,设计时不处理,它就发生在每次生成时。所以设计的动作变了------不问「这里要不要留灵活」,问「这个决策点,让人付记忆成本,还是让模型付错误率」。

最好的答案通常是第三个:都不留。收窄到唯一写法,决策点就不存在了。

判据的用处是当裁决器。提案拿过来,问一句:看这个片段,能不能预测它的行为?能,过。不能,回去重做。

这个判据不新。函数式编程叫它引用透明:把子表达式替换成它的值,程序行为不变。我们把它从表达式层搬到了协议层。局部可解码,是协议版的引用透明。

实证:值形态三态

引擎里的「状态」原本有几种机制:可写值、派生值、异步加载,各有 API,各有名字。模型每写一次状态,就要选一次机制。选错常常不报错,只是行为微妙地不对,错法还各不相同。

重做之后,data 容器里只剩三种值形状,和原来那三种机制一一对应------可写值落成原子,派生值落成 ${...} 串,异步加载落成 {actions, initial}

jsonc 复制代码
{
  "data": {
    "count": 1,                       // 可写原子:直接读写
    "doubleCount": "${count * 2}",    // 只读派生:随引用自动更新
    "userList": {                     // 异步派生:显式动作 + 初值
      "actions": ["fetchUserList"],
      "initial": []
    }
  }
}

语义由形状决定,不再由「你调了哪个 API」决定。

对照改造前。同样是「拼一段展示文案」:

jsonc 复制代码
// ❌ 旧协议:同一个意图两种写法,都合法,都能跑
{ "totalText": "记录共 " + total + " 条" }            // 裸公式
{ "totalText": "${'记录共 ' + total + ' 条'}" }       // 包进 ${...}

// ✅ 新协议:零决策点
{ "totalText": "${'记录共 ' + total + ' 条'}" }       // 形状只剩这一种

形状决定语义。三态删掉的是「选机制」这个动作本身------模型只写最自然的值形状,反应式语义从形状里长出来。这套东西社区有名字,叫 signals;我们没发明它,只把它收窄成三种形状。

想表达「这个值跟着别的值变」,就写 ${...}。没有第二种写法,也就没有第二种错法。

第二维 · 动手:机制与策略

读懂之后要动手。能不能动,动手之前就得知道。我们的答案是一条红线。core 提供机制,引擎担保它的行为:可依赖、有报错、可预测。业务提供策略,那是业务自己的约定:可以动,改了自己负责。中间不许渗。

举个例子,快捷键。core 里有一个 registry:监听、规格化匹配、守卫,纯机制,外加一个绑定入口。哪个键触发什么动作,是业务策略。core 不扫 schema、不解释描述符。

ts 复制代码
registerHotkey({
  key: "cmd+k",                  // 业务策略:哪个键
  scope: "editor",               // 业务策略:在哪生效
  action: "openCommandPalette",  // 业务策略:触发什么------只是个名字,实现在业务侧
});
// 这三个值怎么被监听、匹配、守卫,是 core 的机制;
// core 只提供这一个注册入口,不解释传进来的值

分层不混之后,「这里能不能改」有确定的查法:core 的导出清单就是红线------清单上的是机制,其余全是业务策略。清单有机器看守:多一个没有真实消费者的导出,CI 就拦下。担保范围不许自己膨胀。

机制与策略分离是 UNIX 五十年前的老话,没什么可发明的。但分不清的代价变了。人分不清红线,多问一句同事,是成本;模型不问人,分不清就直接写错。错法很具体:A 页的 cmd+k 是业务自己注册的绑定,模型把它当成引擎自带,到了 B 页直接假设按下去就有命令面板------什么都不会发生,也不报错。

红线划完,还得让它真的生效。模型只产提案,生不生效门禁说了算。 不许动的地方,动了就报错;允许动的地方,过了门禁才合入。没有「建议不动」这种东西。

第三维 · 出错:报错即协议

第三条规矩:不支持的写法必须显式报错,禁止静默兜底。报错算协议的一部分,不是附赠的调试信息。

开头的事故就是兜底兜出来的。兜底的效果,是把错误从生成时的一次报错,变成线上的一次行为异常:前者模型能收到、能修正、下次不重犯;后者没有人知道发生过,包括犯错的那个。

宽容不是没有道理。浏览器那套有名字,叫鲁棒性原则(Postel's law):它的前提是读者能看出来哪儿不对------人看得出来,模型看不出来。写入者没有记忆,这个前提不成立。

对模型来说,我宁要一个明确的报错,也不要一次成功的猜测。

fail-fast 的报错是给人调试的,预期读者带着项目记忆来排查。这里的报错是给模型的下一轮输入设计的,属于协议的一部分,要包含三样:哪里错了,合法的集合是什么,为什么不能这样写。

text 复制代码
❌ Invalid shape: "doubleCount" declared as atom

✅ doubleCount 随 count 变化,应声明为派生形状:${count * 2}。
   原子是一次性快照:不随引用更新,读到的永远是初值。
   要静态值用原子,要跟随变化写 ${...},没有第三种。

「Invalid shape」是给有记忆的人看的:他见过这类错,自己知道去哪查。模型没见过,也没地方查------它拿的是下面那条结构化的,拿着就能在下一轮直接写对。

报错在解析或校验时 fail-loud:过不了门禁不入库。回灌发生在写入当轮------报错作为工具结果当场返回,模型下一轮重写;归档的错误统计只喂协议演化,不进任何模型的上下文。案例逐条喂回上下文,按相似度命中就是把旧错混进新任务;所以教训写成门禁规则,机器强制,不靠模型记住。

给机器消费的结构化报错有先例:RFC 9457 把 HTTP 错误响应标准化成机器可读格式。我们做的事同构,只是消费者从客户端换成了模型。

报错的价值不止于单次修正。错误从静默的行为异常变成显式的生成时报错之后,才谈得上统计和归类。再往后,能机器做的都归机器:跑统计、做归类、起草门禁规则。人只做评审和放行。

发生过一次这样的循环:「该用派生却写了原子」的报错,归类后冒了头。排查发现不是模型的问题,是协议在那个场景的形状不够直觉。下一轮协议修订收窄了原子的适用面,这类报错随之消失。协议开始随错误进化,不再随抱怨。

第四维 · 再来:信号纪律

最后一维:模型下次来,看到什么。

业界主流的答案是管理上下文------压缩、摘要、记笔记、挂外部记忆库,在膨胀的历史里打捞仍算数的部分。

我们的做法是让膨胀不发生。

规矩叫信号纪律(signal discipline):进入工作面的,只有当前算数的东西。工作面,指写入者一次会话真正会读到的那份现场。历史轨迹、决策过程、变更记录,不在供给之列。仓库里没有 ADR(架构决策记录);完成的业务工作流收口之后,执行痕迹物理删除,仍算数的规则沉淀进规范层。

历史有两个去处:git 给「查」,规范层给「用」。上下文只放「用」的。要归因,去 git------按需查阅,不进默认供给。工作面只保留现在。

压缩和摘要的假设是「历史是资产,只是需要管理」。我们的经验相反:对没有记忆的写入者,历史默认是噪声。 检索按相似度排序,不按是否仍然成立排序;加时间衰减也只能压低排名,压不掉「旧方案看起来仍是答案」。旧方案的假设就这样混进新任务------A 域收口后的写法,出现在 B 域的下一次生成里。这类故障已经有人归档命名:context poisoning,错误内容混进上下文,被当成仍成立的答案反复引用。

这不是说压缩没用:单次会话内,历史过长该压还得压。信号纪律管的是下一层------压剩的历史、归档的决策,不许沉淀进工作面。会话窗口归上下文工程管,工作面归协议管。

这一维的完整做法------执行域的生死、收口即删除的全流程、多写入者的并发------是另一篇的事。这里只立判断:时间维度上,系统的义务是提供唯一的「现在」。

四个维度到此走完,写入循环正好闭合一圈:读懂、写入、报错回灌、现场只剩「现在」。下一个写入者到来时,面对的已经是被修正过的现场。

代价与边界

改对了什么:模型生成的 schema,从逐个人工复核才敢合入,变成抽检。抽检敢开,是因为人工复核时代拦下的错误类别,已经陆续写成门禁规则------机器先拦,人抽查漏网的。能被统计,是因为先被看见。

代价也实在。协议表达能力刻意收窄,一些对人「很自然」的写法明确不支持。已有代码有迁移成本:我们的清法是红线先立、存量按触碰慢慢清,不做一次性重写。「不支持即报错」前期还拦下过需求------拦下之后发现,暴露的往往是协议自己的设计缺口,补协议比给单个需求开口子划算。信号纪律同样要付代价:删除执行痕迹靠纪律兜底,沉淀动作没人做,「当前」就会慢慢变成新的历史。

顺带的好处:这套判据对人类新成员同样有效。零记忆的写入者不只有模型,还有刚入职的人。局部可解码的协议,新人第一周就能写对。这不是设计出来的,是自然得到的。

这套东西不是一天设计出来的。判据管方向:新情况出现,回到四个维度倒推,不照抄既有条文。我们自己的引擎里,今天也还有没改完的地方------开头那一类条件写法,就还在名单上。

适用面与自测

适用面三个条件:模型是高频的主要写入者------决策点的错误率乘的是生成次数,低频写入摊不平改造成本;协议面大;写错代价高。全占,值得做。AI 只是偶尔辅助的系统、协议面极小的工具库、探索期的原型,不必做:原型期最重要的是跑起来,可解码性是规模化之后的约束。

自测只需要一个想象:一个对项目零记忆、只读得到当前片段的写入者,能不能只看片段就写对?能,过了。不能,缺的就是这一层。

结语

工具会越来越强。强模型不会替代环境,只会放大环境。

上下文工程优化「喂什么」,是对的,也是不够的。系统的下一场考试,不是「人能不能用」,是「没有记忆的智能,能不能只看一眼就写对」。

回到开头那份 schema。这套判据是一批一批执行的:已经改完的地方,交上去过不了门禁------报错写明合法集合和为什么;开头那一类条件写法,就在下一批。错误在写入当场就发出声音,靠的是四条规矩:可解码性、机制与策略、报错即协议、信号纪律。

引擎是公司的,不开源。判据的完整实物随引擎留在公司,公开仓库里能给你看的是另一件:陷空山------一个由 AI 执行器在明文纪律下协作交付的产品,规则、协议、进度、记忆四份文件就摆在仓库里。平台侧的实现细节,不在本文范围。

坐标深圳,做平台与 AI 方向。这类问题,欢迎交流。

相关推荐
流光D33 分钟前
AI时代,搭建 web 站点并配置 nginx 反向代理流程
运维·服务器·前端·人工智能·nginx·ai·ai编程
idcu1 小时前
CodeSchema 开源首发:一个给 AI 编码助手「喂」精准代码上下文的索引服务
开源·go·ai编程
七牛云行业应用1 小时前
DSH Desktop 桌面版实测:Win/macOS 一键安装,“桌面也是插件“怎么理解,和 npx 起 Web UI 差在哪
人工智能·agent·ai编程
DaPang8191 小时前
跟 AI 说人话,比给它填表格管用
ai编程
易岳群1 小时前
Vibe Coding 与 Skills、Agent、Prompt:构建 AI 辅助编程的新工作流
ai·prompt·ai编程
旖旎夜光1 小时前
【AI入门】大模型介绍全解析:从模型、LLM 到提示词与嵌入
人工智能·笔记·python·学习·ai编程
学习星球2 小时前
2026 AI编程大转向:从Vibe Coding到Agentic Engineering,人从执行循环中抽离意味着什么?
ai编程
漂流瓶jz10 小时前
【AI】大模型本地部署与量化:Ollama、transformers、llama.cpp实践
人工智能·llm·ai编程