大模型的提示词写多了以后,很多人会有一种感觉:同一个任务,换一种说法,结果就完全变了。更麻烦的是,当多个模型 Agent 互相调用时,A 发出的消息 B 不一定能理解,因为两边都在用自由自然语言。Canon 这个词在这里与打印机、相机品牌无关,它指的是一套面向模型提示(model prompting)和 Agent 间通信(agent-to-agent communication)的受控英语(Controlled English)。受控英语不是让机器学会自然语言,而是反过来,让人和 Agent 在用词、句式和语义上先收敛到一个可预测的子集里。
本文会围绕 Canon 的设计思路,先解释受控英语解决什么问题,再给出一个可落地的 Canon 语法子集,然后用它分别编写模型提示和 Agent 通信消息,最后提供一个轻量校验模块、运行验证方法和常见问题排查清单。学完后,你可以把同一套语法应用到自己的提示词工程和多 Agent 协作场景中,让提示更可复现,让消息更容易被解析和校验。
1. 先理解受控英语:为什么提示和 Agent 通信需要受限语言
1.1 什么是受控英语
受控英语(Controlled English)是自然语言的一个受限子集,它在词汇、语法、句式上做了明确规定,只允许使用者按规则写出符合要求的句子。它在工业界已经有成熟的先例,例如航空航天领域的简化技术英语 ASD-STE,以及自然语言处理研究中的 Attempto Controlled English(ACE)。这些语言的共同点是:看起来像英语,读起来像英语,但机器可以用规则精确解析,不会因为歧义产生多种理解。
放在大模型场景里,受控英语的价值会被放大。模型提示词本质上是一段自然语言指令,Agent 之间的消息本质上是自然语言请求和响应。自由自然语言有丰富的表达力,但也带来歧义、漂移和不可验证的问题。Canon 的思路就是给提示和消息定一套规则,让内容在可控的表达范围内流动。
1.2 自由自然语言提示的三类问题
第一类是歧义。同一个句子在不同上下文里有不同含义,例如“请处理数据”到底是清洗、转换、删除还是统计,模型只能靠猜测。第二类是漂移。同一个任务用不同措辞表达,模型的输出格式、粒度和风格可能完全不同,这导致下游解析困难。第三类是不可验证。自由文本没有强约束,你很难在请求发出前检查它是否完整、是否包含禁忌内容、是否指定了输出格式。
这三类问题在单模型提示中就已经存在,在 Agent 通信中会被放大。Agent A 用一段含混的话请求 Agent B 查询库存,Agent B 返回的结果可能缺少关键字段,A 又得再问一遍。最终整个系统的行为是概率性的,很难定位问题出在哪个环节。
| 问题 | 典型现象 | Canon 的应对方式 |
|---|---|---|
| 歧义 | 同一指令被模型理解成不同任务 | 限定动词集合和固定句型 |
| 漂移 | 输出格式、粒度每次不同 | 显式声明 output_format 约束 |
| 不可验证 | 消息发出前无法检查是否合规 | 词法与句法层做机器校验 |
| 意图不明 | Agent 收到消息不知道要做什么 | 强制使用 action 字段声明动作 |
1.3 Canon 的核心定位:提示模板与消息契约的统一语言
Canon 的一个关键设计是把“模型提示”和“Agent 通信”放在同一套语言框架下。提示词可以看作人给模型的一条 Canon 消息,Agent 之间的请求响应也可以看作 Agent 之间交换的 Canon 消息。这样有一个直接好处:提示词校验器、消息解析器、模板渲染器可以共用一套代码。
Canon 的设计目标可以概括为四点:受限词表、固定句型、显式参数、机器可查。受限词表控制动词和修饰词的范围;固定句型限制表达方式;显式参数把变量从自然语言中抽出来,避免模型自由发挥;机器可查则保证任何一段 Canon 文本都可以在发送前被程序校验。
2. Canon 语法设计:一套可解析的受限英语子集
这里给出的 Canon 语法是围绕标题所述目标整理的一个可实践版本,不是唯一的官方定义。实际项目落地时,需要根据所用模型的能力和业务场景调整词表和句型。
2.1 词法层:动词、参数与禁用表达
词法层负责定义哪些词可以用。动词是 Canon 的核心,因为动词决定了消息的意图。下面是一组最常用的 Canon 动作动词。
| 动词 | 语义 | 典型场景 |
|---|---|---|
| request | 请求对方执行动作 | Agent 之间发起任务 |
| confirm | 确认信息或任务已收到 | 响应请求 |
| reject | 拒绝请求并给出原因 | 无法执行时 |
| report | 返回事实或结果 | 任务完成后的结果上报 |
| query | 查询某个属性 | 信息检索场景 |
| notify | 主动通知事件发生 | 事件驱动协作 |
| retry | 要求对方重试 | 失败恢复 |
| cancel | 取消已发出的请求 | 任务撤销 |
| complete | 声明任务已完成 | 任务收尾 |
| fail | 声明任务失败 | 异常上报 |
除了动词,词法层还定义参数占位符、数据类型和修饰词。参数统一用[参数名]表示,数据类型用<类型>表示,例如<int>、<float>、<bool>、<date>、<path>、<enum:a|b|c>。修饰词则限制语义,例如within 30 seconds表示时限,must与must not表示义务和禁止。
2.2 句法层:Canon 的七种基本句型
句法层定义消息可以长成什么样。Canon 把消息分为请求、报告、查询、决策、通知、确认、错误七类,每一类都对应一种固定句型。固定句型的作用是让解析器可以用规则或正则直接判断消息类型,而不需要依赖模型理解。
- 请求:
request TARGET with PARAMS within TIME - 报告:
report: SUBJECT is VALUE - 查询:
query: ATTRIBUTE of SUBJECT - 决策:
decision: OPTION selected - 通知:
notice: EVENT occurred - 确认:
ack: MESSAGE_ID received - 错误:
error: ACTION failed because REASON
例如,一个库存查询请求可以写成request inventory_check with sku=A1001 quantity=2 within 30 seconds。解析器不需要理解业务,只需要匹配request开头、随后是目标、with后跟参数对、within后跟时间限制,就能得到结构化结果。
2.3 语义约束层:否定、条件、时限与角色
语义约束层回答“这句话在什么条件下成立、对谁成立”。Canon 对否定做了严格要求:只能使用must not和cannot,禁止使用双重否定和含混否定词。条件表达使用固定形式if CONDITION then ACTION,并且条件本身要写成可判定的表达式,例如if stock_level < 10 then notify restock。
时限统一使用within加时长或绝对时间戳,避免“尽快”“稍后”这类模糊表达。角色则通过from、to、reply_to字段显式指定,消息发送方、接收方和回复对象不能隐藏在自然语言上下文里。这样做的原因是:Agent 之间协作时,上下文窗口有限,不能靠“记住上一句话”来推断角色。
3. 用 Canon 编写可复现的模型提示
3.1 一个 Canon Prompt 的最小结构
模型提示可以看作发送给模型的一条 Canon 元消息。推荐的最小结构包含版本号、角色、任务、输入、输出格式和安全约束。以下是一个 YAML 风格的 Canon Prompt 示例。
canon-prompt: v1.0 role: assistant task: summarize subject: meeting_notes input: text: <text> max_words: 150 output_format: heading: summary bullets: true include_fields: [结论, 待办] guardrail: must_not_include: [人名, 报价, 情绪化用语] must_include: [结论, 待办] language: chinese这个结构有几个关键点。role和task是必填的,告诉模型身份和任务类型;input里把自由文本作为参数传入,而不是拼接在指令里;output_format明确输出结构;guardrail声明哪些内容必须出现、哪些必须禁止。这样写的好处是,模型即使出现输出漂移,校验程序也能根据guardrail判断输出是否合格。
3.2 从自由文本提示改写为 Canon 提示
很多人的第一版提示词是这样写的:
你是一个有帮助的助手。请阅读这段很长的会议记录,给我一个简洁的摘要。 不要包含太多数字,尽量短一些。语气要专业一点。这段提示至少有四个问题:没有说明摘要包含哪些字段、没有指定“短”的具体长度、没有定义“专业”的量化标准、“不要包含太多数字”也没有明确数字上限。用 Canon 改写后,每个约束都变成可检查的规则。
canon-prompt: v1.0 role: assistant task: summarize input: text: <text> max_words: 150 max_numbers: 3 style: professional output_format: headings: [结论, 待办] bullets: true guardrail: must_not_include: [情绪化用语] must_include: [结论, 待办]改写背后的逻辑是:把人类的模糊期望翻译成程序可以校验的规则。max_words: 150替代“尽量短”,max_numbers: 3替代“不要太多数字”,style: professional作为模型风格参数而不是自然语言修饰词。
3.3 Canon 提示验证脚本与预期输出
编写 Canon 提示后,可以在发送给模型前先做一次静态校验。校验脚本检查必需字段是否齐全、参数类型是否合法、guardrail 是否有冲突。例如max_words: 150和min_words: 200同时出现就是冲突,脚本应当直接报错。
校验通过后,模型的输出也应该进入同一套校验体系。如果模型返回的摘要包含人名,而guardrail.must_not_include中声明了人名,则该输出应该被拒绝并要求重试。这样,提示词就不再是一次性文本,而是可测试的配置。
4. 用 Canon 设计 Agent 到 Agent 的通信协议
4.1 为什么只有 JSON 还不够
多 Agent 系统常见的做法是定义 JSON 消息结构,例如{ "action": "query", "params": {...} }。JSON 解决了字段结构化的问题,但没有解决语义结构化的问题。action的值到底可以取哪些枚举?params里哪些字段必填?query和report的语义边界在哪里?这些问题 JSON Schema 能约束一部分,但无法约束“这句话表达的真实意图”。
Agent 之间真正需要的是语言级契约。Canon 消息就是这种契约的载体:它规定了动词集合、句型结构、参数绑定和时序规则。接收方不必依赖模型猜测意图,而是通过解析规则直接得到意图和参数。
4.2 Canon Message 的结构与字段说明
一条完整的 Canon Message 由信封部分和业务部分构成。信封部分负责路由、标识、时限和回复关系,业务部分负责具体动作。
canon-message: v1.0 from: agent_a to: agent_b msg_id: msg-20250101-001 action: request subject: inventory_check payload: sku: A1001 quantity: 2 within: 30 seconds reply_to: msg-20250101-001字段含义如下:
| 字段 | 必填 | 含义 |
|---|---|---|
| canon-message | 是 | 协议版本,决定解析规则 |
| from | 是 | 发送方标识 |
| to | 是 | 接收方标识 |
| msg_id | 是 | 消息唯一标识 |
| action | 是 | 动作动词,必须是词表中的枚举 |
| subject | 是 | 业务主题,决定 payload 的语义 |
| payload | 条件必填 | 动作参数,结构由 subject 决定 |
| within | 否 | 超时时间,表示要求在多久内响应 |
| reply_to | 否 | 回复的目标消息 ID |
| if | 否 | 执行条件 |
注意if字段是可选但重要的:它把条件从自然语言里抽出来,变成显式表达式。例如if: stock_level < 10表示只有当库存低于 10 时才执行该动作。
4.3 最小双 Agent 通信示例
下面模拟两个 Agent 之前的协作。Agent A 请求 Agent B 检查库存,Agent B 返回报告,如果参数不合法则返回错误消息。
Agent A 发出的请求:
canon-message: v1.0 from: agent_a to: agent_b msg_id: msg-20250101-001 action: request subject: inventory_check payload: sku: A1001 quantity: 2 within: 30 secondsAgent B 库存充足,返回报告:
canon-message: v1.0 from: agent_b to: agent_a msg_id: msg-20250101-002 action: report subject: inventory_check payload: sku: A1001 available: 5 fulfilled: true reply_to: msg-20250101-001Agent B 库存不足或参数非法,返回错误:
canon-message: v1.0 from: agent_b to: agent_a msg_id: msg-20250101-003 action: fail subject: inventory_check payload: reason: quantity_negative reply_to: msg-20250101-001在这个示例中,双方不需要对话历史就能理解彼此。action决定了消息类型,subject决定了业务含义,payload的字段由subject对应的 schema 决定。整个协议是可解析、可校验、可追溯的。
5. 落地实现:一个轻量 Canon 解析与校验模块
下面用 Python 实现一个最小可运行的 Canon 解析与校验模块。它的目标是验证一段 Canon 消息是否符合语法,并输出结构化结果或错误列表。
5.1 项目结构与依赖
canon/ __init__.py lexicon.py grammar.py validator.py renderer.py examples/ prompt_validator_demo.py agent_chat_demo.py依赖只需要 Python 3.8 以上标准库,不需要第三方包。lexicon.py定义动词表和数据类型,grammar.py定义句型规则,validator.py负责解析和校验,renderer.py负责把模板渲染成最终消息。
5.2 词法与句型规则实现
lexicon.py核心代码如下:
# canon/lexicon.py CANON_VERBS = { "request": "REQUEST", "confirm": "CONFIRM", "reject": "REJECT", "report": "REPORT", "query": "QUERY", "notify": "NOTIFY", "retry": "RETRY", "cancel": "CANCEL", "complete": "COMPLETE", "fail": "FAIL", } ALLOWED_TYPES = {"int", "float", "bool", "date", "path", "text"} def is_valid_verb(word: str) -> bool: return word.lower() in CANON_VERBS def is_valid_type(value: str) -> bool: return value in ALLOWED_TYPESgrammar.py中定义动作与句型的映射关系:
# canon/grammar.py ACTION_PATTERNS = { "request": r"^request\s+[a-z_]+(\s+with\s+.+)?(\s+within\s+.+)?$", "report": r"^report:\s*[a-z_]+ is .+$", "query": r"^query:\s*[a-z_]+ of [a-z_]+$", "fail": r"^fail:\s*[a-z_]+ because [a-z_]+$", } def match_action(line: str) -> str | None: for action, pattern in ACTION_PATTERNS.items(): if re.match(pattern, line.strip(), re.IGNORECASE): return action return None这里的关键点是规则先于模型。Canon 不要求模型理解消息,而是要求消息匹配规则。match_action返回的动作就是后续路由和处理的依据。
5.3 模板渲染与参数绑定
实际使用中,Canon 消息通常由模板渲染生成,而不是手写。renderer.py负责把模板中的占位符替换为实际参数:
# canon/renderer.py from string import Template def render_canon_prompt(template: str, params: dict) -> str: tpl = Template(template) return tpl.safe_substitute(params) def render_canon_message(verb: str, subject: str, payload: dict, meta: dict) -> str: lines = [ "canon-message: v1.0", f"from: {meta['from']}", f"to: {meta['to']}", f"msg_id: {meta['msg_id']}", f"action: {verb}", f"subject: {subject}", ] if payload: for key, value in payload.items(): lines.append(f" {key}: {value}") if meta.get("within"): lines.append(f"within: {meta['within']}") if meta.get("reply_to"): lines.append(f"reply_to: {meta['reply_to']}") return "\n".join(lines)渲染和解析是同一个语言规则的两面。渲染时按照规则生成文本,解析时按照规则拆回结构。只要规则一致,生成器和解析器就不会出现“说的人以为说清楚了,听的人以为没听懂”的问题。
5.4 消息校验与错误返回
validator.py提供两条入口:校验提示词配置,校验消息文本。
# canon/validator.py import re def validate_message(raw: str) -> dict: errors = [] required_fields = ["canon-message", "from", "to", "msg_id", "action", "subject"] lines = [line.strip() for line in raw.strip().splitlines() if line.strip()] parsed = {} for line in lines: if line.startswith("canon-message"): parsed["version"] = line.split(":")[1].strip() elif ":" in line and not line.startswith(" "): key, _, value = line.partition(":") parsed[key.strip()] = value.strip() else: # 处理 payload 缩进字段 pass for field in required_fields: if field not in parsed: errors.append(f"missing field: {field}") if parsed.get("action") not in CANON_VERBS: errors.append(f"invalid action: {parsed.get('action')}") if parsed.get("within"): if not re.match(r"^\d+\s+(seconds|minutes|hours)$", parsed["within"]): errors.append("invalid within format") return { "valid": len(errors) == 0, "parsed": parsed, "errors": errors, }调用示例如下:
raw = """canon-message: v1.0 from: agent_a to: agent_b msg_id: msg-20250101-001 action: request subject: inventory_check within: 30 seconds""" result = validate_message(raw) print(result["valid"]) print(result["errors"])正常输出是True和空列表。如果漏掉subject或把within写成as soon as possible,就会得到对应的错误信息。这就是“机器可查”的落地形态。
6. 运行验证:从提示执行到 Agent 通信的完整链路
6.1 学习环境验证步骤
先在一个最小环境里跑通 Canon 的生成、校验、解析三个环节。
python -m canon.examples.prompt_validator_demo python -m canon.examples.agent_chat_demo验证时观察三件事。第一,模板渲染出的文本是否符合 Canon 语法;第二,校验器对合法消息返回valid: True,对非法消息返回具体错误;第三,两个 Agent 的示例在交换消息后,接收方能否正确解析出action、subject和payload。不要只验证程序能启动,还要故意构造几条非法消息,确认校验器真的能拦截。
6.2 开发与测试环境需要观察的指标
进入开发测试阶段,建议记录以下指标。
| 指标 | 含义 | 健康值参考 |
|---|---|---|
| 解析成功率 | 合法消息中被正确解析的比例 | 接近 100% |
| 校验命中率 | 非法消息被拦截的比例 | 接近 100% |
| 模型输出合规率 | 模型输出符合 Canon 约束的比例 | 越高越好 |
| 重试次数 | 因输出不合规而重试的次数 | 越低越好 |
| 端到端时延 | 请求发出到结果返回的时间 | 满足业务要求 |
如果解析成功率低,先检查消息生成器,大概率是渲染模板写错了。如果模型输出合规率低,说明 Canon 约束超出了模型的理解能力,需要简化句型或补充示例。
6.3 生产环境需要的额外保障
生产环境不能只在代码层面做校验。至少还要补齐配置外置化、日志追踪、版本兼容、权限隔离和回滚方案。
Canon 语法本身是版本化的,消息中携带v1.0就是为了做多版本兼容。生产环境建议采用“新旧版本并存、逐步迁移”的策略,先让所有 Agent 按新语法生成,同时接受旧语法的流量解析,再逐步下线旧规则。日志方面,每条消息的msg_id应当贯穿请求链路,方便在问题出现时按 ID 检索。
7. 常见问题排查:提示走样、Agent 互相听不懂、校验失效
7.1 问题一:模型输出不再符合 Canon 约束
现象:明明在提示里写了must_not_include,模型还是输出了禁止内容。
可能原因有三个。第一,模型上下文太长,尾部约束被稀释;第二,约束表达不够突出,被淹没在一大段指令里;第三,约束本身与模型能力不匹配,例如要求模型严格输出 YAML,但模型常常夹带解释性文字。
检查方式:把最终发送给模型的完整提示打出来,确认约束确实在上下文窗口内;用小样本测试不同位置放置约束的效果;查看模型输出日志,确认不合规的具体内容是什么。
解决方案:把guardrail放在提示词的最后一段,这是模型输出前最容易被注意到的位置;如果有多个约束,只保留最关键的三四个;必要时增加结构化输出或 JSON Mode 作为兜底。
7.2 问题二:Agent 之间消息无法理解
现象:A 收到 B 的消息后,解析出action为空,或者字段对不上。
常见原因是双方使用了不同版本的 Canon 词表。A 的词汇表里有notify,B 的词汇表里没有;B 发送的 payload 字段名是sku_id,A 的 schema 要求的是sku。另一个常见原因是消息里混入了自由文本,例如 B 在 payload 里写了note: please check carefully,这会让解析器难以判定字段边界。
检查方式:打开双方的消息日志,逐条核对canon-message版本号;对比双方法册的动词表、字段表和枚举值。
解决方案:统一用一个共享的 schema 仓库管理词表和字段定义;消息发送前,发送方先做一次本地校验;遇到无法识别的字段时,接收方返回fail消息而不是静默忽略。
7.3 问题三:校验规则莫名其妙失效
现象:规则看起来没问题,但校验器就是报错,或者漏过了非法消息。
常见的原因是文本格式问题。YAML 风格的 Canon 文本中,字段名和冒号之间多了空格、payload 缩进不一致、消息末尾残留了换行符,都可能干扰正则匹配。另一个原因是正则表达式写得太宽松,例如.+匹配了不该匹配的内容,导致非法格式也通过校验。
检查方式:打印消息文本的原始字节,确认没有不可见字符;用最小的合法消息逐步增加字段,找到触发错误的那一项。
解决方案:不要把校验逻辑写得太复杂,优先使用固定模板渲染加字段级校验;处理 payload 时用独立的数据结构,而不是依赖缩进解析;给每条规则写一组正反用例,防止正则回归。
7.4 排查顺序
按以下顺序排查,能覆盖绝大部分问题:
- 消息是否是按模板渲染生成的,而不是手工拼写。
- 消息的
canon-message版本双方是否一致。 - 动词、字段名、枚举值是否在共享词表中注册。
- 字段缩进、冒号、换行是否符合语法规则。
- 正则与校验脚本是否有正反用例覆盖。
- 模型输出侧是否有额外的解释性文字混入。
- 是否已经查看日志中的完整消息原文。
8. Canon 语法设计的最佳实践与扩展方向
8.1 受控英语设计检查清单
设计一套属于自己的受控英语时,建议逐项核对以下清单。
- 动词表是否控制在 10 到 20 个之间,是否覆盖了业务主要动作。
- 每种动作是否有唯一对应的固定句型。
- 参数是否使用显式占位符,而不是嵌在自然语言里。
- 是否明确定义了数值单位、时间格式、枚举范围。
- 是否声明了禁止表达,例如否定词、模糊时间词、情绪化用语。
- 是否提供模板渲染器和解析校验器两个配套工具。
- 是否维护了版本号,并且允许新旧版本并存。
- 是否为正反用例写了自动化测试。
8.2 自由文本与 Canon 的取舍
受控英语不是要取代自由文本,而是在需要确定性的时候提供约束。两者各有适用场景。
| 维度 | 自由自然语言 | Canon 受控英语 |
|---|---|---|
| 表达力 | 高,适合开放生成 | 低,只覆盖预定义语义 |
| 可解析性 | 低,依赖模型理解 | 高,规则可确定性解析 |
| 可验证性 | 弱,难以预检 | 强,发送前可校验 |
| 上手成本 | 低 | 中高,需要维护词表和模板 |
| 适用场景 | 创意生成、开放对话 | 任务调度、多 Agent 协作、结构化输出 |
实际项目中,推荐采用分层的做法:外部用户可以用自由语言输入,内部先经语义解析映射到 Canon 消息,再交给 Agent 执行。这样既保留了用户体验,又获得了内部通信的确定性。
8.3 扩展方向
Canon 可以从三个方向扩展。一是领域词表定制,在电商、物流、客服、运维等场景中增加领域动词和字段;二是生成式 DSL,在 Canon 合法语句的基础上,自动生成 SQL、Jinja 模板或 API 调用代码;三是多语言受控语言,在中文、日文等语言上实现与 Canon 等价的受限子集,再通过标准格式互相转换。
对于刚接触受控英语的开发者,建议先从一个最小的双 Agent 协作场景开始,定义 5 个动词、3 个句型、10 个字段,跑通生成、校验、解析、执行、返回的完整链路,再逐步扩展。这样既能理解 Canon 的核心价值,也不会在初期被复杂的词表维护拖住。受控英语不是限制模型的想象力,而是把需要确定性的部分锁住,把需要创造力的部分留给模型。这是提示工程从“拼运气”走向“可工程化”的关键一步。