尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Canon-A 受控英语:让多智能体通信稳定、提示词再不漂移

Canon-A 受控英语:让多智能体通信稳定、提示词再不漂移 搞定多智能体通信不稳定、提示词漂移从 Canon-A 受控英语开始。先回顾一下我踩过的一个真实问题在有多个大模型 Agent 协同工作的项目里每个 Agent 的 prompt 都写着“请用 JSON 返回”但模型偶尔会给出多余的说明文字、Markdown 代码块甚至把字段名从小写改成大写。下游解析器一崩整个链路跟着失败。后来引入了一套接近 Canon-A 的受控英语约束方式让每个 Agent 的输入输出都被限制在固定词汇表和固定句型里问题立刻减少了很多。这篇文章就把这套方法论拆开讲清楚。1. 背景与核心概念1.1 什么是受控英语Controlled English受控英语并不是新概念。在航空航天、机械维修等领域为了确保技术文档足够清晰、没有歧义早就出现了 Simplified Technical English简化技术英语STE这类规范。它的核心思想是不允许使用完整自然语言的全部表达能力而是只使用预设的词汇、固定的句式和明确的语法结构。把同样的思路迁移到 AI 场景就出现了 Canon-A 这类实践。它要求给模型看的提示词、以及智能体之间传递的消息都遵守一个可校验的受限语言规范。换句话说自由自然语言Please tell me the weather in Beijing tomorrow, and also maybe if it will rain?受控英语QUERY weather cityBeijing datetomorrow两者表达的信息可能差不多但第二种写法有明确的结构机器可以稳定解析。1.2 Canon-A 的核心定位Canon-A 本质上是一套面向 LLM 提示词和 Agent 通信的受控英语规范设计思路。它强调三件事词汇受控定义一份白名单词汇表模型输出时只能用这些词。句型受控消息必须符合固定的句法模板比如动作 对象 参数。输出可校验每条输出都能用解析器验证非法输出直接拒绝。这样做的目的不是让模型“变笨”而是让模型的输出从“自然语言”降级为“结构化协议语言”从而获得更高的可预测性和稳定性。1.3 它解决什么问题在实际的大模型应用里最常见的四类问题是问题原因Canon-A 的应对方式输出格式漂移模型对自由 prompt 理解不一致强制固定句型和词汇字段命名不一致模型自己“发挥”白名单词汇表校验Agent 间消息不可解析下游收到非结构化文本统一消息模板无法审计和回溯自然语言语义模糊受限语言可程序化校验所以Canon-A 更准确的定位是提示词工程和 Agent 通信之间的“共享协议层”。它解决的不是“模型懂不懂”而是“我们能不能稳定地读取模型的输出”。1.4 应用场景Canon-A 比较适合以下场景多智能体协作系统中的消息传递。需要下游程序直接消费模型输出的业务链路。需要长期维护、频繁迭代的 Prompt 项目。对输出可解释性和安全审计有要求的系统。如果是写聊天机器人、写创意文案这类开放场景就不太适合用 Canon-A因为输出本来就应该开放。2. 环境准备与示例项目结构Canon-A 本身不是一个固定的 SDK目前更多是一套规范思路。不同团队落地时会有不同实现。本文用一个轻量 Python 示例演示完整流程方便你直接跑通。2.1 环境说明Python 3.9 及以上版本。只用 Python 标准库无需安装额外依赖。如果你需要调用大模型 API可以自行安装requests或对应的官方 SDK。操作系统不限Windows / macOS / Linux 均可。2.2 示例项目结构我建议按下面的目录结构组织代码canon-a-demo/ ├── canon/ │ ├── __init__.py │ ├── grammar.py # 受控英语语法定义 │ ├── parser.py # Canon-A 消息解析器 │ ├── validator.py # 词汇与模板校验器 │ └── message.py # Agent 间消息数据类 ├── agents/ │ ├── __init__.py │ ├── base.py # Agent 基类 │ ├── planner.py # 规划 Agent 示例 │ └── tool_agent.py # 工具调用 Agent 示例 ├── prompts/ │ └── system_prompt.txt # 给模型的受控提示词模板 ├── examples/ │ └── run_demo.py # 完整运行示例 └── README.md下面的内容会逐个文件讲解。3. 核心语法拆解以 Canon-A 的思路为基础定义一套最小的受控语言子集。这里把规则简化成四部分词汇表、动作类型、句法模板、参数格式。3.1 词汇表Wordlist词汇表是所有 Canon-A 表达式的“字母表”。只有出现在白名单里的词才允许出现在消息中。# 文件路径canon/grammar.py # 动作词白名单 ACTION_WORDS { QUERY, # 查询 REQUEST, # 请求执行 INFORM, # 告知结果 CONFIRM, # 确认 DENY, # 拒绝 ERROR, # 错误 } # 对象词白名单 TARGET_WORDS { weather, # 天气 time, # 时间 calculator, # 计算器 database, # 数据库 order, # 订单 stock, # 库存 } # 参数键白名单 PARAM_KEYS { city, date, unit, expression, order_id, limit, } # 通用限定词 MODIFIER_WORDS { now: True, today: True, tomorrow: True, yes: True, no: True, }核心思路是词汇表是代码仓库里的一份受版本控制的配置而不是模型手册里的一段描述。新增词汇要走评审流程。3.2 动作类型Canon-A 里的每条消息都必须以动作词开头。动作词决定了消息的意图动作含义使用场景QUERY查询信息Agent 向另一个 Agent 询问数据REQUEST请求执行操作Agent 请求工具执行动作INFORM返回结果Agent 返回查询结果CONFIRM确认确认任务已收到或完成DENY拒绝拒绝非法请求ERROR错误上报错误信息动作词是句子的“根”解析器拿到消息后会先提取动作词再进行后续处理。3.3 句法模板Canon-A 消息统一使用以下模板ACTION TARGET keyvalue keyvalue举个例子QUERY weather cityBeijing datetomorrowREQUEST calculator expression11INFORM weather cityBeijing datetomorrow resultclearERROR database order_id1001 reasontimeout这种模板的好处是没有嵌套结构没有自由文本没有从句。参数用keyvalue平铺顺序不再重要。3.4 基础解析器实现解析器负责把 Canon-A 消息拆成结构化对象。# 文件路径canon/parser.py import re from dataclasses import dataclass, field from typing import Dict from .grammar import ACTION_WORDS, TARGET_WORDS, PARAM_KEYS class CanonAParseError(Exception): Canon-A 解析异常 dataclass class CanonAMessage: 解析后的 Canon-A 消息 action: str target: str params: Dict[str, str] field(default_factorydict) raw: str def to_text(self) - str: 重新生成文本保证消息可复现 parts [self.action, self.target] for key, value in self.params.items(): parts.append(f{key}{value}) return .join(parts) def parse_canon_a_message(text: str) - CanonAMessage: 从文本解析 Canon-A 消息 text text.strip() if not text: raise CanonAParseError(empty message) tokens text.split() action tokens[0] if action not in ACTION_WORDS: raise CanonAParseError(funknown action: {action}) target None params {} for token in tokens[1:]: if in token: key, value token.split(, 1) if key not in PARAM_KEYS: raise CanonAParseError(funknown param key: {key}) params[key] value else: if target is None: target token else: raise CanonAParseError(funexpected token: {token}) if target is None: raise CanonAParseError(missing target) if target not in TARGET_WORDS: raise CanonAParseError(funknown target: {target}) return CanonAMessage( actionaction, targettarget, paramsparams, rawtext, )解析之后任何下游程序都能安全地读取action、target、params这三个字段。4. 用 Canon-A 约束模型提示词把 Canon-A 用到模型提示词里的思路是不要求模型听懂复杂的自然语言而是要求模型只能输出 Canon-A 句子。4.1 System Prompt 模板这一步是核心中的核心。我在prompts/system_prompt.txt中定义了一份模板你是一个严格遵守受控语言 Canon-A 的助手。 规则 1. 你的输出只能是 Canon-A 格式消息。 2. Canon-A 格式为ACTION TARGET keyvalue keyvalue 3. 可用的 ACTION 包括QUERY, REQUEST, INFORM, CONFIRM, DENY, ERROR 4. 可用的 TARGET 包括weather, time, calculator, database, order, stock 5. 可用的参数键包括city, date, unit, expression, order_id, limit 6. 如果用户请求的内容无法映射到上述词汇请输出DENY order reasonunsupported 7. 如果工具执行失败请输出ERROR order reasonexecution_failed 现在开始回答用户问题时只输出 Canon-A 消息不要输出任何其他解释。注意这里的关键点把模板也变成白名单的一部分。规则列表本身是固定的模型没有自由发挥空间。4.2 调用大模型并做校验下面演示一个实际调用流程。这里以 HTTP API 为例你也可以根据实际使用的模型 SDK 做替换。# 文件路径examples/run_demo.py import os import requests from canon.parser import parse_canon_a_message, CanonAParseError def load_system_prompt() - str: 读取 System Prompt 模板 with open(prompts/system_prompt.txt, r, encodingutf-8) as f: return f.read() def call_llm(user_input: str) - str: 调用大模型返回模型输出文本 这里以 OpenAI Chat Completions 兼容接口为例。 实际使用时请将 base_url 替换为你的服务地址。 api_key os.getenv(LLM_API_KEY) if not api_key: raise RuntimeError(环境变量 LLM_API_KEY 未设置) response requests.post( https://api.openai.com/v1/chat/completions, # 按实际服务地址调整 headers{ Authorization: fBearer {api_key}, Content-Type: application/json, }, json{ model: gpt-4o-mini, # 按你账号可用模型调整 messages: [ {role: system, content: load_system_prompt()}, {role: user, content: user_input}, ], temperature: 0, }, timeout30, ) response.raise_for_status() return response.json()[choices][0][message][content] def main(): # 示例输入正常查询 user_input 北京明天天气怎么样 output call_llm(user_input) print(模型原始输出:, output) # 归一化并校验 output output.strip() try: msg parse_canon_a_message(output) except CanonAParseError as e: print(解析失败输出非法:, e) return print(解析结果:) print( action:, msg.action) print( target:, msg.target) print( params:, msg.params) if __name__ __main__: main()运行前设置 API Keyexport LLM_API_KEY你的密钥 python examples/run_demo.py预期输出大概是模型原始输出: QUERY weather cityBeijing datetomorrow 解析结果: action: QUERY target: weather params: {city: Beijing, date: tomorrow}4.3 为什么 temperature 要设置为 0在 Canon-A 工作流里推荐把温度设置为 0。原因很简单受控语言的目的是稳定输出不是创造性输出。如果温度过高模型可能会改变单词大小写、换用同义词导致校验失败。这也引出了一个实践原则当你在用受控语言时尽量不要把模型当成“创作工具”而是当成“转换函数”。5. 智能体间通信从自由文本到 Canon-A 协议接下来模拟两个 Agent 之间的通信。这里不依赖真实 LLM而是用规则引擎模拟保证例子可以直接运行。5.1 消息数据类与校验器消息类和校验器可以进一步封装。# 文件路径canon/message.py from dataclasses import dataclass, field from typing import Dict dataclass class AgentMessage: Agent 间传递的消息信封 sender: str receiver: str request_id: str canona_message: str timestamp: float 0.0 extra: Dict[str, str] field(default_factorydict)# 文件路径canon/validator.py from .grammar import ACTION_WORDS, TARGET_WORDS, PARAM_KEYS from .parser import CanonAMessage def validate_canon_a_message(msg: CanonAMessage) - list: 返回所有校验错误空列表表示通过 errors [] if msg.action not in ACTION_WORDS: errors.append(faction 不在白名单: {msg.action}) if msg.target not in TARGET_WORDS: errors.append(ftarget 不在白名单: {msg.target}) for key in msg.params: if key not in PARAM_KEYS: errors.append(f参数键不在白名单: {key}) # 根据动作类型做额外检查 if msg.action QUERY and msg.target weather: if city not in msg.params: errors.append(QUERY weather 必须提供 city 参数) if msg.action REQUEST and msg.target calculator: if expression not in msg.params: errors.append(REQUEST calculator 必须提供 expression 参数) return errors这里展示了一个重要思路校验不只是词汇层面的检查还可以针对业务规则写约束。比如天气查询必须带城市计算器请求必须带表达式。5.2 Agent 基类Agent 基类负责收消息、解析、校验、分发。# 文件路径agents/base.py from canon.message import AgentMessage from canon.parser import parse_canon_a_message, CanonAParseError from canon.validator import validate_canon_a_message class Agent: 所有 Agent 的基类 def __init__(self, name: str): self.name name def receive(self, message: AgentMessage) - str: 接收消息返回回复的 Canon-A 文本 if message.receiver ! self.name: return ( ERROR order reasonreceiver_mismatch fsender{message.sender} receiver{message.receiver} ) try: parsed parse_canon_a_message(message.canona_message) except CanonAParseError as e: return fERROR order reasonparse_failed detail{e} errors validate_canon_a_message(parsed) if errors: return fERROR order reasonvalidation_failed detail{errors[0]} return self.handle(parsed, message) def handle(self, parsed, message: AgentMessage) - str: 子类实现具体业务逻辑 raise NotImplementedError5.3 规划 Agent 与工具 Agent规划 Agent 把一段需求拆成 Canon-A 指令发给工具 Agent工具 Agent 执行并返回结果。# 文件路径agents/tool_agent.py import time from .base import Agent from canon.message import AgentMessage class ToolAgent(Agent): 工具 Agent处理天气查询和计算器请求 def handle(self, parsed, message: AgentMessage) - str: if parsed.target weather: city parsed.params.get(city, ) date parsed.params.get(date, now) # 模拟天气查询 result sunny if city Beijing else cloudy return ( fINFORM weather city{city} date{date} fresult{result} ) if parsed.target calculator: expression parsed.params.get(expression, ) try: # 安全计算方法这里只用四则运算演示 # 不要在生产环境直接使用 eval需做严格校验 result self._safe_calculate(expression) except Exception as e: return ( fERROR calculator expression{expression} freasoninvalid_expression detail{e} ) return fINFORM calculator expression{expression} result{result} return fERROR {parsed.target} reasonunsupported_target def _safe_calculate(self, expression: str): 简易安全计算只允许数字和 - * / 空格 import re if not re.fullmatch(r[0-9\-*/(). ], expression): raise ValueError(contains invalid characters) # 警告eval 有风险生产环境必须使用安全表达式解析库 return eval(expression)# 文件路径agents/planner.py from .base import Agent class PlannerAgent(Agent): 规划 Agent把用户意图转成 Canon-A 指令 def handle(self, parsed, message: AgentMessage) - str: # 在真实项目中这里会调用 LLM 做意图识别 # 这里用规则模拟 if 天气 in message.extra.get(user_input, ): return REQUEST planner targetweather_agent actionquery cityBeijing return DENY order reasonno_matching_intent5.4 完整通信示例# 文件路径examples/run_agent_demo.py from agents.planner import PlannerAgent from agents.tool_agent import ToolAgent from canon.message import AgentMessage def main(): planner PlannerAgent(planner) tool_agent ToolAgent(tool_agent) # 模拟一个用户请求 req_msg AgentMessage( senderuser, receiverplanner, request_idreq-001, canona_messageREQUEST planner actionplan, extra{user_input: 北京天气}, ) # 1. 规划 Agent 生成指令 plan_text planner.receive(req_msg) print(规划结果:, plan_text) # 2. 把规划结果作为消息发给工具 Agent # 注意这里为了演示直接构造一条 Canon-A 消息 tool_msg AgentMessage( senderplanner, receivertool_agent, request_idreq-001, canona_messageQUERY weather cityBeijing datetomorrow, ) # 3. 工具 Agent 执行并返回 result tool_agent.receive(tool_msg) print(工具返回:, result) if __name__ __main__: main()运行这段代码python examples/run_agent_demo.py预期输出规划结果: DENY order reasonno_matching_intent 工具返回: INFORM weather cityBeijing datetomorrow resultsunny你会发现规划 Agent 的规则模拟还不够聪明没有从user_input里提取出cityBeijing。在真实项目里这里就会用 LLM 来做自然语言到 Canon-A 的转换。这正是 Canon-A 的核心玩法之一LLM 负责把自由文本翻译成受控文本规则代码负责执行受控文本。6. 常见问题与排查思路在实际使用 Canon-A 或类似受控英语方案时我整理了一些高频问题问题现象常见原因解决思路模型输出总是被校验器拒绝词汇表太小模型找不到合适表达适当扩充白名单尤其是参数 value 部分模型输出了白名单之外的单词System Prompt 约束不够强在 prompt 中加入“非法词汇即报错”的示例同一请求多次输出不同格式temperature 设置过高将 temperature 设为 0消息解析时出现多余字段模型往 keyvalue 里塞了带空格的值在 prompt 中要求值不能含空格或用引号包裹智能体出现循环请求没有设置最大通信轮数在 Agent 基类中加入 max_hops 限制validator 报错但不知哪条规则失败校验错误信息不够详细把返回的错误列表改成结构化的错误对象生产环境调用 LLM 超时没有做重试和降级增加超时、重试、熔断机制其中最容易忽视的是第一条词汇表太小时模型会“强迫自己”输出白名单外的词。比如你把city放进参数键白名单但用户城市名是San Francisco如果你没有限制参数值必须单词语模型可能输出citySan Francisco结果解析时整个句子被拆成两个 token直接报错。解决方案有两个方向在 prompt 中明确参数值不允许包含空格遇到多词城市用下划线连接比如citySan_Francisco。允许参数值用引号包裹比如citySan Francisco然后在解析器里支持引号切分。我更推荐第二种因为可读性更好也不容易触发 token 切分问题。7. 最佳实践与工程建议7.1 词汇表当代码管理Canon-A 的语法文件、词汇表、校验规则应该像代码一样放在 Git 仓库里走 Code Review。任何新增词汇都意味着通信协议变更必须有理由、有测试、有文档。7.2 每一轮通信都带 request_id多智能体系统里消息一多就难以追踪。建议每一轮 Agent 间通信都带上request_id并在日志里打印完整的消息流转链路。# 建议在 Agent.receive 中打印结构化日志 import logging logger logging.getLogger(__name__) logger.info( agent_message, extra{ request_id: message.request_id, sender: message.sender, receiver: message.receiver, canona: message.canona_message, }, )7.3 安全边界不要让 Agent 执行任意指令Canon-A 虽然限制了语言结构但没有限制语言背后的行为。如果 Agent 收到REQUEST calculator expression...背后如果直接执行了系统命令那就是严重的安全风险。校验器一定要做业务级安全控制表达式白名单只允许数字、四则运算符、小括号。命令白名单只允许授权工具。最小权限Agent 运行账号不能有文件删除、数据库 DROP 等权限。生产环境变更必须经过审批流程并在测试环境验证。7.4 降级策略模型输出偶尔会被校验器拒绝这是常态。生产系统里要设计降级策略重试一次给模型多一次机会把校验错误信息也传回模型让它修正。转人工重试仍失败时把原始输出和错误原因发送到人工处理队列。兜底回复面向用户的场景回复“暂时无法处理请稍后再试”。7.5 对语法版本做兼容Canon-A 的语法会迭代。Agent 升级后可能发出新版本的 Canon-A 消息而旧 Agent 还停留在旧解析器上。建议在消息中带上协议版本号QUERY weather protocol1.2 cityBeijing datetomorrow解析器检查版本号后再决定用哪套解析规则。这能避免多智能体系统上线时的“协议爆炸”。8. 总结与下一步学习建议这篇文章详细拆解了 Canon-A 受控英语的设计理念以及在模型提示和多智能体通信中的落地方式。核心收获可以总结为四句话受控英语的价值在于让模型输出可预测、可解析、可校验。词汇表、动作词、句法模板必须用代码管理而不是写在文档里。LLM 负责从自由文本到受控文本的转换规则代码负责执行受控文本。校验器不只是检查格式还应该包含业务规则和安全边界。如果继续深入建议按下面的顺序学习阅读 Simplified Technical EnglishSTE的规范了解受控语言的工程背景。研究 JSON Schema 与 Pydantic 这类结构化校验工具思考它们与 Canon-A 的异同。在真实项目中从最细粒度的单个 Agent 输出校验开始再逐步扩展到多 Agent 通信协议。尝试把 Canon-A 与低代码工作流编排、可观测性日志体系结合起来。可能有人会觉得 Canon-A 有些“笨重”多了一层规范少了很多灵活性。但从我的经验看当系统里有三个以上 Agent 协作、每个月都要迭代 Prompt 时这层“笨重”反而是最值得的投资。如果你也在做多智能体系统建议先用一个最小的用例跑通这套流程再慢慢扩充词汇表。把语法文件当作代码一样评审你会少踩很多解析和联调的坑。
返回列表