这里的 Canon,不是相机品牌,也不是打印机驱动,而是一套把模型提示词和 Agent 之间通信统一到“受控英语”上的方案。项目标题里的关键词很直接:controlled English for model prompting and agent-to-agent communication,也就是用一套受限、规则化的英语子集,去约束大模型的输入指令,也约束多个 Agent 之间的消息传递。我最近在实际链路里试过类似的思路,最大的感受是:它解决的不是“模型能不能听懂”的问题,而是“模型每次听懂的结果是否一致、是否可解析、是否可以稳定交接”的问题。
如果你正在做 LLM 应用、Prompt 工程,或者打算搭一套多 Agent 协作系统,这篇文章值得往下看。最值得关注的点不是语法本身,而是这种“受限表达”能帮你把不可控的自然语言交互,收敛成可测试、可回放、可批量验证的工程链路。下面按实际落地顺序拆开讲。
1. 先搞清楚 Canon 这类受控英语解决什么问题
1.1 模型提示词为什么需要“受限表达”
大模型最强的地方是开放语言理解,最麻烦的地方也是开放语言理解。同一句话,换个说法,模型可能给出不同动作;同一段业务指令,今天跑和明天跑,结果也可能不一样。
举个很常见的例子:
帮我查一下订单,然后如果没问题就发货,顺便把物流单号告诉我。这句话人看没问题,但交给模型以后,至少有四个模糊点:“查一下订单”没有说查哪个订单;“没问题”到底是什么标准;“发货”走哪家物流;“顺便”意味着优先级低,可能被模型忽略。
在闲聊场景里,这种模糊可以容忍。但在生产链路里,每个模糊点都可能变成一次错误执行。Canon 这类受控英语的思路,就是主动砍掉这种自由度。它把允许使用的动词、字段名、条件判断、响应格式都限定住,模型只能在有限的组合空间里生成内容。这样做好处很直接:输出可预测了,错误可定位了,测试可自动化了。
1.2 Agent 之间为什么不能直接自由对话
多 Agent 系统里,问题会更明显。假设你有一个规划 Agent、一个执行 Agent、一个复核 Agent,它们之间如果靠自由文本互相传消息,每一轮都会发生一次“二次理解”。
A 告诉 B:
It looks okay, ship it.B 接收到之后,首先要判断 “it” 指什么,“okay” 的判断标准是什么,“ship” 要调哪个接口。如果这个系统只有两个 Agent,还能靠模型上下文硬撑。一旦链路变成五个 Agent、十轮交接,每轮都有信息损耗,最后得到的输出可能已经和原始目标偏差很大。
Canon 的作用,是给 Agent 之间定一套“双方都认的消息契约”。执行方不需要再靠意图猜测,而是直接解析受控语句里的动词、对象、条件、字段值。这比自由文本更可靠,又比纯 JSON Schema 更接近自然语言,人看日志的时候不会觉得像在读乱码。
1.3 它不是用来替代所有提示词,而是给关键链路加“紧箍咒”
有一点要先说清楚:受控英语不是要废掉自然语言 Prompt。创意写作、头脑风暴、开放式问答,这些场景强行套受控语法反而会削弱模型能力。Canon 最适合的场景是那些“出错了代价很高”的关键链路。
我自己的判断标准是三条:
- 这个任务是否会被高频重复执行?
- 这个任务的输出是否会被程序解析和二次使用?
- 这个任务如果出错,是否会造成资金、库存、权限、数据上的实际影响?
如果三个都满足,就值得用受控英语。如果只是让模型帮忙写一段文案,那保持自由表达就好。Canon 更准确的定位,是给关键链路上的动作和响应加一道“紧箍咒”,而不是把整条路都铺成固定轨道。
2. 从普通提示词到受控英语:设计思路与最小语法
2.1 受控英语的典型组成
受控英语并不是“把句子写简单”这么简单。它更像是一套微型语法体系,核心要控制住四样东西。
第一是动作动词。每个动作都对应一个明确的系统操作,比如 query、check、create、update、ship、return、notify。动词不能随意扩展,能不用近义词就不用近义词。今天用 “query order”,明天就不要改成 “look up order” 或 “fetch order”。
第二是对象和字段名。order、inventory、user、shipment、invoice 这些对象要有统一定义。字段名也要固定,比如 order_id、tracking_id、carrier、stock_count。字段名模糊,解析逻辑就会跟着混乱。
第三是条件表达。受控英语里最常见的就是where条件,用来限定查询或动作范围。比如where order_id = 'SO-2024-001'。这里的关键是值类型保持一致,字符串统一用单引号,数字直接写,日期用 ISO 8601。
第四是响应格式。模型回答也要受控。不能说一堆解释,而是直接回答指定字段,比如respond with order_status, tracking_id。响应格式一旦明确,下游脚本就能稳定解析。
这里给出一套最小示例语法,先不要追求完整,够用就行:
action := query | check | create | update | ship | return | notify | confirm object := order | inventory | user | shipment | invoice | task condition := field = 'value' | field > number | field < number response := respond with field [, field ...]2.2 Canon 式表达的基本语法约定(示例)
Canon 项目本身在标题里没有给出具体语法文档,所以下面给的是我在类似受控英语实践里常用的一套写法,不代表 Canon 的最终规范。真正落地时以你使用的实现文档为准,但设计原则是通用的。
示例语句:
query order where order_id = 'SO-2024-001' check inventory for sku = 'KB-2210', quantity = 3 if stock_count >= 3 then ship order where order_id = 'SO-2024-001' via carrier = 'standard' respond with order_status = 'shipped', tracking_id = 'SF123456'这些语句有几个共同特点:
- 动词放在最前面,一眼就能看出意图。
- 对象紧跟在动词后,明确操作对象。
where用来写过滤条件。respond with明确告诉模型“你只需要回答这些字段”。- 字符串字段统一加单引号,避免空格和特殊字符干扰解析。
如果一个业务动作无法用这套结构表达,那通常不是语法不够,而是这个动作本身还没被定义清楚。这时候先回业务侧把输入和输出敲定,不要急着往语法里加新句式。
2.3 一个最小可运行示例:把模糊业务描述改写成 Canon 表达
还是回到上面那个模糊需求:
帮我查一下订单,然后如果没问题就发货,顺便把物流单号告诉我。把它拆成 Canon 表达,可以写成四步:
query order where order_id = 'SO-2024-001' check order where risk_flag = 'low' ship order where order_id = 'SO-2024-001' via carrier = 'standard' return tracking_id, estimated_delivery_date每一步对应一个确定动作,每个动作的输入参数都是明确字段。模型不再需要猜测“没问题”是什么意思,因为风险判断已经被收敛成risk_flag = 'low'这样一个可检查条件。
喂给模型的提示词可以这样组织:
You must respond in Canon. Allowed actions: query, check, ship, return. All fields must use these names: order_id, risk_flag, carrier, tracking_id, estimated_delivery_date. Response example: query order where order_id = 'SO-2024-001'注意,这里没有让模型自己发挥,而是明确告诉它“必须用这种格式回答”。刚开始模型可能还会多输出解释性文字,这很正常,需要跑几次之后,一边调整提示词一边收紧输出格式。
3. 在 Prompt 中使用受控英语:实操流程与参数判断
3.1 单条 Prompt 的改写步骤
我建议把每次改写都走成固定五步,不要凭感觉写。
第一步,提取真实意图。先不去管用户怎么说,而是问:这个请求最终要触发哪个系统动作?查订单?改状态?发货?通知?
第二步,找出实体和条件。订单号是多少?库存量是多少?风险等级阈值是多少?这些信息如果在用户原话里没有,就要通过上下文补齐,或者在 Prompt 外围先做一轮字段抽取。
第三步,映射到允许的动作。把真实意图对应到受控英语的动词表里。如果动词表里没有,就先停下来,确认这个动作是不是真的需要支持。
第四步,定义响应字段。明确“模型回答里必须包含哪些字段”。这一步很关键,很多解析失败不是因为模型笨,而是因为 Prompt 没有规定回答结构。
第五步,跑一条测试。先不管复杂场景,拿一条最典型的输入试跑,看输出能不能被脚本解析成预期结构。能跑通,再继续扩。
3.2 判断 Prompt 是否“受控”成功的标准
判断一套受控 Prompt 成不成功,不能只看“模型这次回答对不对”。要有一套可量化的判断维度。
我一般会看四个指标:
- 可解析率:输出能否被脚本成功转换成结构化数据。比如用正则或简单解析器,把
status = 'shipped'解析成字典。 - 字段齐全度:
respond with要求的字段是否全部出现,有没有漏字段。 - 语义一致性:同一输入跑三次,结果是否一致。
- 输出长度:回答是否稳定在较短范围内。如果模型每次都额外解释一大段,说明受控还没到位。
| 指标 | 看什么 | 判断标准 |
|---|---|---|
| 可解析率 | 输出能被脚本结构化成功的比例 | 低于 90% 不要进批量 |
| 字段齐全度 | 响应字段是否都存在 | 必须 100% |
| 语义一致性 | 同一输入多次结果是否稳定 | 结果应基本一致 |
| 输出长度 | 回答的 token 数是否稳定 | 应该短且稳定 |
如果四个指标都不达标,先别急着调模型参数,优先检查 Prompt 里有没有给出足够的格式约束和示例。很多时候加一个respond with模板,比调温度参数管用得多。
3.3 批量改写和模板化:让提示词可复用
单条跑通之后,下一步是把受控英语做成模板,让它能批量处理。这里不是让大家写复杂代码,而是先把“动作、字段、响应格式”从 Prompt 里抽出来,变成一份可维护的配置。
一份最小配置可以长这样:
allowed_actions: - query order - ship order - return tracking_id response_fields: - tracking_id - estimated_delivery_date temperature: 0 max_tokens: 128 stop_sequences: - "\n"代码侧只需要把用户输入里的字段值填充进模板,然后统一调用模型接口。这里有一个很关键的经验:批量任务不要一上来就开最大并发。我的习惯是先用 5 到 10 条样例跑一遍,记录每条的解析结果和失败原因,确认输入字段都是干净的,再逐步提高并发。
批量任务里最常见的错误不是模型崩了,而是数据源里某些字段为空、格式不对、订单号带了多余空格。输入不干净,Prompt 再怎么受控都没用。
4. Agent 之间通信为什么要一套“双方都认的 Canon 语言”
4.1 自由文本协议在高频协作里的问题
如果只是单个 Agent 完成任务,方案可以很随意。但 Agent 一多,消息就成了关键瓶颈。
自由文本协议看起来灵活,实际跑起来问题很多。接收方需要通过模型做二次理解;理解错了,后面的动作就全错。而且自由文本缺少稳定的状态码,问题排查只能靠人肉读日志。比如执行 Agent 返回一句 “sorry, it failed”,规划 Agent 根本不知道下一步该重试、换方案还是终止任务。
这时候如果用结构化 JSON 做消息,机器解析是简单了,但模型生成 JSON 时容易出现字段拼错、嵌套错误、多一个逗号少一个括号的问题。而且纯 JSON 的可读性差,排错时要来回对照字段名。
受控英语正好卡在两者之间。它保持了接近自然语言的可读性,同时因为语法受限,又可以被确定性解析器处理。
4.2 Canon 消息的常见结构
Agent 之间通信时,我一般不会让整条消息都变成纯文本,而是采用“JSON 外壳 + Canon 载荷”的结构。
外壳负责寻址和路由,里面塞的是一段受控英语语句。这样机器可以靠外壳里的 sender、receiver、intent 快速转发,模型或解析器再处理里面的 Canon 载荷。
{ "sender": "planner", "receiver": "executor", "intent": "ship_order", "request_id": "req_12345", "payload": "ship order where order_id = 'SO-2024-001' via carrier = 'standard'" }这个结构的好处是:定位快速,日志可读,负载不高。排查问题时,直接看 payload 就能明白 Agent 之间发生了什么;程序处理时,也能按 intent 路由到对应处理器。
4.3 场景示例:两个 Agent 用 Canon 语言完成一次任务交接
下面是一个简化的协作示例。
规划 Agent 想确认库存,于是发送:
check inventory for sku = 'KB-2210', quantity = 3执行 Agent 处理完后返回:
inventory_result sku = 'KB-2210' available = true, reserved_quantity = 3规划 Agent 收到结果,确认可以发货,于是发送:
ship order where order_id = 'SO-2024-001' via carrier = 'standard'执行 Agent 返回:
shipped order_id = 'SO-2024-001', tracking_id = 'SF123456'如果库存不足,执行 Agent 返回的错误也应该遵循统一格式:
error code = 'INSUFFICIENT_STOCK', message = 'only 2 in stock'这套方式最明显的提升是:任何一个环节出错,都可以通过日志复现整条链路。哪条消息格式不规范、哪个字段缺失、哪个 Agent 没有按约定返回结果,一眼就能定位。相比自由文本,它把 Agent 之间的“对话损耗”降到了最低。
5. 落地时如何验证效果:环境、样例、指标、排查
5.1 先从小样例开始验证
我不建议一上来就构造一个超复杂的多 Agent 场景。受控英语的效果验证,应该从最小样例开始。
先挑 5 到 10 个有代表性的任务。所谓代表性,不是最难的那几个,而是最常用的那几个。比如查订单、查库存、改状态、生成物流单号。把这些任务分别写成 Canon 表达,然后跑单轮测试。
单轮测试通过后,再跑多轮 Agent 交接。多轮测试时,每一轮的输入都要用上一轮的输出,这时最容易看到 Canon 表达是否真的稳定。如果第二轮开始出现格式漂移,就要回到 Prompt 和语法定义里找原因。
5.2 评价指标看什么
受控英语的验证指标,和普通 Prompt 质量的验证指标不太一样。普通 Prompt 更看重内容质量,受控英语更看重结构稳定性和可解析性。
| 指标 | 看什么 | 我的参考线 |
|---|---|---|
| 可解析率 | 输出能否被脚本转成结构化结果 | 90% 以上再进批量 |
| 字段齐全度 | 要求返回的字段是否都在 | 必须 100% |
| 语义一致性 | 同一输入跑多次是否一致 | 结果基本稳定 |
| 输出长度 | 回答的 token 数是否可控 | 持续稳定或下降 |
| 失败恢复率 | 非法输出后重试的成功率 | 越高越好 |
这里要提醒一句:指标只能帮你判断“当前方案适不适用”,不能脱离业务场景直接抄数值。如果你的任务本身输入千奇百怪,初期可解析率低于 90% 很正常,先把输入清洗和字段规范化做好,再回头看指标。
5.3 常见报错和排查顺序
接触受控英语的初期,最常遇到的报错其实就那么几类。
第一类,模型仍然输出大段自然语言。原因通常是 Prompt 里没有明确限制输出格式,或者没有给出足够少的示例。解决方法是把respond with模板加进系统提示词,并给两三条完整示例。
第二类,输出里出现未知动作词。比如你只定义了 ship、query,模型却输出了 dispatch。这说明动作词表还不够聚焦,需要在 Prompt 里强调“只能用以下动作词”。
第三类,必填字段缺失。比如要求返回 tracking_id,结果没有返回。解决方法是把响应字段写进模板,并且用“必须包含”这样的强约束表达。
第四类,解析器在转义或引号上出错。这通常不是模型问题,而是输入数据里的字符串带了特殊符号。先清洗数据,再进模型。
第五类,值在业务系统里查不到。这种情况即使模型生成得再标准,下游执行依然会失败。排查时要先确认输入数据是不是已经过时,或者字段是不是填错。
通用排查顺序我建议这样走:
- 先看现象:是报错、卡住、无输出,还是输出格式不对。
- 再看输入:字段是否为空、编码是否正确、路径是否完整。
- 再看 Prompt:动作词、字段名、响应格式是否约束到位。
- 再看参数:temperature、max_tokens、stop_sequences 是否符合解析需求。
- 最后看运行环境:依赖版本、权限、接口状态、并发是否过高。
这条链路里,最容易被忽略的是第二步。很多问题看起来是模型不会生成,实际上是把脏数据喂给了模型。
6. 边界与经验:不是所有场景都适合受控英语
6.1 哪些场景适合 Canon,哪些不适合
适合用受控英语的场景,通常具备三个特征:重复、结构化、出错成本高。比如订单管理、库存查询、物流状态更新、任务编排、API 参数生成、信息抽取。这些场景里的输入输出边界清晰,适合用固定动词和固定字段描述。
不适合用受控英语的场景是:开放对话、创意写作、情感支持、头脑风暴、探索性分析。这些任务本身就依赖语言多样性,强行受控会牺牲表达质量,反而给用户带来机器感。
如果你是个人开发者,只是想跑通一个几十行的小工具,受控英语不一定有显著收益。但如果你在做团队项目,或者准备把多个 Agent 长期部署到生产环境,那从第一天就定好受控表达规范,会比后期返工省很多事。
低配环境也能尝试。受控英语只是 Prompt 层面的约束,不依赖额外算力。但如果你要把它做成完整的 Agent 通信协议,那除了语言规范,还要考虑消息队列、日志、失败重试、状态管理这些工程问题。
6.2 从 Prompt 模板到微调:项目演进路径
大部分团队不需要一开始就微调模型。受控英语的落地,我更推荐按阶段推进。
第一阶段,用系统提示词加几个示例,把动作词、字段名、响应格式写清楚。大多数业务场景下,这个阶段已经能解决 80% 的问题。
第二阶段,把 Prompt 做成模板,加上校验脚本和失败重试。模型偶尔输出不合法内容时,自动重试一次或两次,能明显提升整体成功率。
第三阶段,如果任务量很大,且模型在受控英语上的稳定性一直达不到要求,再考虑收集“原始输入到 Canon 输出”的数据,做一次小规模微调。
第四阶段,如果 Agent 之间需要高可靠通信,再引入正式语法解析器,对每条 Canon 语句做严格校验。
不要跳步。我自己见过不少团队,刚跑通两三个用例就急着微调、急着上并发,最后往往卡在数据清洗和效果评估上,回头还得重建 Prompt。
6.3 我建议的落地清单
如果现在准备在项目里引入 Canon 这类受控英语方案,我建议按以下清单逐项检查。
- 动作动词是否控制在 10 个以内,且没有近义词混用。
- 字段名是否有统一定义,字符串、数字、日期格式是否固定。
- 响应格式是否明确,模型是否知道“只需要回答哪些字段”。
- 是否准备了 5 到 10 条代表性测试样例。
- 每条测试样例是否跑了至少三次,确认结果稳定。
- 输出是否可以被脚本稳定解析,而不是靠人眼判断。
- 是否有日志记录原始输入、模型输出、解析结果。
- 是否对非法输出做了重试机制。
- 批量任务前是否检查过输入数据质量。
- 是否保留了自由文本场景的入口,而不是一禁了之。
踩过几次之后我发现,很多问题不是工具能力不够,而是前置环境和输入材料没有处理干净。Canon 这类受控英语给模型搭了一个框架,但框架能不能起作用,取决于你有没有把动作、字段、响应格式先定义清楚。先跑稳单条,再上批量;先定协议,再写解析器,这个顺序不要反过来。