1. 这不是术语辨析题,而是Agent系统设计的分水岭
“Agent Skill 和 Rules 有什么区别?”——这个问题在2024年中后期的AI工程实践中,已经从一个初学者的困惑,演变成团队架构师在评审Agent系统方案时必问的“灵魂拷问”。我去年参与三个不同规模的Agent项目:一个面向金融合规审核的内部工具、一个电商客服智能体集群、还有一个嵌入IDE的编程辅助Agent。三支团队在初期都用“Skill”和“Rules”混着写,结果无一例外在第二迭代周期陷入严重瓶颈:行为不可控、调试像盲人摸象、上线后策略漂移。直到我们把这两个概念从代码层、设计层、运维层彻底剥离开,系统才真正进入可演进状态。
核心差异一句话说透: Skill是Agent的“肌肉记忆”,Rules是Agent的“交通法规” 。肌肉记忆决定它 能做什么 (比如调用天气API、解析PDF表格、生成SQL),而交通法规决定它 在什么条件下、以什么方式、对谁、做多少次 (比如“仅当用户身份为VIP且请求含‘加急’关键词时,才调用高成本OCR服务”)。这个区分不是语义游戏,它直接对应到代码结构、测试策略、灰度发布流程甚至SLO指标定义。
你可能在Cursor、Claude Code或Hermes Agent的文档里看到它们被并列罗列,甚至有些框架(比如早期Codex)把Rules硬编码进Skill内部。但实测下来,这种耦合会让系统在三个月后变得无法维护。我见过最典型的案例:某团队把“禁止向未认证用户返回数据库原始错误信息”这条安全规则,直接写死在了数据库查询Skill的catch块里。后来业务要求对内网员工放宽该限制,他们不得不复制整个Skill,改两行代码,再维护两个几乎一样的版本——这就是混淆Skill与Rules的代价。
关键词“Agent”“Skill”“Rules”之所以成为热搜,恰恰说明行业正从“能跑通Demo”阶段,迈入“要长期稳定交付”的深水区。本文不讲抽象定义,只拆解我们在真实项目中如何落地分离:从代码目录结构怎么组织,到Rule引擎选型时为什么放弃YAML硬编码而采用表达式树,再到线上Rule变更如何做到毫秒级生效且不重启Agent进程。所有内容,都来自踩坑后的血泪复盘。
2. Skill的本质:封装确定性能力的最小可执行单元
2.1 Skill不是功能模块,而是带契约的“服务原子”
很多团队把Skill理解成“一个Python函数”或“一个HTTP接口”,这是最大的认知偏差。真正的Skill必须满足三个硬性契约,缺一不可:
-
输入输出契约 :明确声明支持的参数类型、格式约束、必填项;输出必须有结构化Schema(如JSON Schema),而非字符串拼接。例如一个“提取合同关键条款”的Skill,其输入契约必须规定
document_type: enum["pdf", "docx", "txt"],输出契约必须定义clauses: array[{name: string, content: string, confidence: number}]。我们曾因忽略confidence字段的数值范围定义,在下游做阈值过滤时引发整条流水线误判。 -
执行边界契约 :声明超时时间、重试策略、失败降级路径。这不是可选项——当Skill调用外部API时,必须预设
timeout_ms=3000且max_retries=2,否则Agent主循环可能被单个慢请求拖垮。在电商客服项目中,我们强制所有Skill实现get_execution_boundaries()方法,由Agent Runtime统一注入熔断器。 -
可观测性契约 :每个Skill调用必须产生标准日志事件(含trace_id、skill_name、input_hash、duration_ms、status)和至少一个Prometheus指标(如
skill_invocations_total{skill="extract_clauses",status="success"})。没有这些,你就永远不知道是Skill本身有问题,还是上游传参错了。
提示:不要用装饰器或AOP框架动态注入这些契约。我们试过用
@track_skill装饰器,结果在异步Skill中丢失trace_id。最终方案是让Skill类继承BaseSkill抽象基类,所有契约检查在__init__和execute方法中硬编码。看似啰嗦,但保证了100%覆盖率。
2.2 Skill的代码结构:为什么必须隔离“能力”与“逻辑”
看一个反面案例——某团队写的“发送邮件”Skill:
# ❌ 危险:Rules逻辑污染Skill本体
class SendEmailSkill:
def execute(self, to, subject, body):
# 规则1:VIP用户优先走企业邮箱
if self.is_vip(to):
smtp_server = "vip.smtp.company.com"
else:
smtp_server = "default.smtp.company.com"
# 规则2:含敏感词的邮件需人工审核
if self.contains_sensitive_words(body):
self.queue_for_review(to, subject, body)
return {"status": "pending_review"}
# 规则3:每日发送上限50封
if self.get_daily_count(to) >= 50:
raise RateLimitExceeded()
# 真正的发送逻辑(仅占20行)
return send_smtp(smtp_server, to, subject, body)
这个Skill看似功能完整,实则埋下三颗雷:
-
is_vip()等规则判断依赖外部服务,一旦该服务延迟,整个Skill阻塞; - 规则变更(如新增“白名单域名免审”)必须修改Skill代码,触发全量回归测试;
- 无法对“VIP路由”规则单独压测或灰度。
正确做法是剥离出纯能力层:
# ✅ Skill只做一件事:发送邮件
class SendEmailSkill(BaseSkill):
def __init__(self, smtp_config: dict):
super().__init__()
self.smtp_config = smtp_config # 配置注入,非硬编码
def execute(self, to: str, subject: str, body: str) -> dict:
# 严格校验输入
validate_email(to)
assert len(subject) <= 200
# 执行发送(无任何业务规则)
result = send_via_smtp(
server=self.smtp_config["server"],
port=self.smtp_config["port"],
auth=self.smtp_config["auth"],
to=to,
subject=subject,
body=body
)
return {"status": "sent", "message_id": result.message_id}
# ✅ Rules引擎负责决策:何时调用?用哪个Skill?
# 规则配置(JSON/YAML/DB表均可)
rules = [
{
"id": "vip_email_route",
"condition": "user.vip_level > 2 and request.channel == 'email'",
"action": {"type": "route_to_skill", "skill": "SendEmailSkill", "config": {"server": "vip.smtp.company.com"}}
},
{
"id": "sensitive_word_review",
"condition": "contains_any(request.body, ['银行卡', '身份证号'])",
"action": {"type": "queue_for_review"}
}
]
注意:Skill的
execute方法里绝对不能出现if/else业务分支。所有分支逻辑必须外移到Rules引擎。这是检验Skill是否合格的黄金标准。
2.3 Skill的生命周期管理:从注册到退役的四阶段
Skill不是写完就扔进仓库的静态资产,它有明确的生命周期,每个阶段都需要配套机制:
| 阶段 | 关键动作 | 我们的实践 |
|---|---|---|
| 注册 | Skill元数据入库(名称、版本、契约、作者、SLA承诺) | 用Git Hook自动扫描 skills/ 目录下的 skill.yaml ,提交PR时触发CI验证契约完整性 |
| 发现 | Agent运行时动态加载Skill | 不用 importlib 硬导入,而是通过 SkillRegistry 中心注册表,按需加载。避免启动时加载全部Skill导致内存暴涨 |
| 调用 | Runtime注入上下文(user_id、session_id、trace_id) | 在 BaseSkill.execute() 中强制接收 context: dict 参数,禁止Skill自行获取全局变量 |
| 退役 | 标记为deprecated,设置迁移窗口期 | 当新Skill替代旧版时,在Registry中标记 deprecated_since="2024-06-01" ,并在调用时记录告警日志,30天后自动禁用 |
我们曾因跳过“退役”阶段,导致一个已废弃的 LegacyPDFParserSkill 在生产环境静默运行半年,其不兼容新版PDF库的bug引发大量解析失败,却因日志被淹没而未被发现。现在所有Skill注册时必须填写 deprecation_policy 字段,这是硬性准入门槛。
3. Rules的本质:定义Agent行为边界的动态策略系统
3.1 Rules不是配置文件,而是可执行的“行为合约”
把Rules当成YAML配置是另一个常见误区。真正的Rules系统必须满足:
- 可计算性 :每条Rule必须能被编译成可执行的表达式树(AST),而非字符串模板。例如
user.balance > 10000 and request.intent == "loan"必须能转为AST节点,才能被高效求值。 - 可组合性 :多条Rule能按优先级、作用域、条件交集等方式组合。比如“VIP用户享受免密支付”和“单笔交易超5万需二次验证”必须能同时生效,且后者优先级更高。
- 可追溯性 :每次Rule匹配必须记录完整的决策链(which rule matched, why condition passed, what action triggered),用于审计和问题定位。
我们弃用过三种Rule方案:
- 纯YAML配置 :简单但无法处理复杂条件(如嵌套
and/or/not、函数调用),且每次变更需重启Agent; - 数据库规则表 :灵活但查询性能差,高并发下成为瓶颈;
- 硬编码在Service层 :完全违背开闭原则。
最终选择自研轻量级Rule引擎,核心是将Rule编译为字节码:
# Rule定义(DSL)
rule "high_value_user_discount"
when user.vip_level >= 3 and cart.total_amount > 5000
then apply_discount(0.15)
priority 100
# 编译后字节码(简化示意)
[
LOAD_ATTR "user.vip_level",
CONST 3,
COMPARE_OP ">=",
LOAD_ATTR "cart.total_amount",
CONST 5000,
COMPARE_OP ">",
AND,
JUMP_IF_FALSE 10,
CALL_FUNCTION "apply_discount" [0.15],
...
]
这套字节码在Agent启动时编译一次,后续匹配速度比解释执行快8倍,且支持热更新——上传新Rule DSL,引擎自动重新编译字节码并替换,毫秒级生效。
3.2 Rules的四大核心维度:为什么必须立体化设计
Rules绝非单一维度的“if-else”,它需要四个正交维度协同定义行为边界:
| 维度 | 说明 | 实例 | 我们的实践 |
|---|---|---|---|
| 作用域(Scope) | Rule生效的上下文范围 | global (全系统)、 agent:customer_service (特定Agent)、 user:12345 (单用户) | 用Redis Hash存储Scope映射, scope:user:12345 指向该用户的专属Rule列表,支持AB测试 |
| 优先级(Priority) | 冲突时的裁决顺序 | 数值越大越优先, priority 100 覆盖 priority 50 | 优先级不设上限,但强制要求每条Rule注明 conflict_with 字段,明确声明与哪些Rule可能冲突 |
| 时效性(Validity) | 生效/失效时间窗口 | valid_from: "2024-06-01T00:00:00Z", valid_until: "2024-06-30T23:59:59Z" | 时间判断用UTC,避免时区混乱;引擎每分钟检查一次Rule时效性,自动归档过期Rule |
| 可观测性(Traceability) | 决策过程可审计 | 记录 rule_id , matched_at , input_snapshot , decision_path | 所有Rule匹配日志接入ELK,用KQL可查“过去1小时所有被触发的VIP相关Rule” |
提示:不要在Rule条件中写
now() > '2024-06-01'这类硬编码时间。正确做法是Rule引擎在执行时注入context: {current_time: datetime.utcnow()},条件写成context.current_time > rule.valid_from。这样既保证时间一致性,又便于单元测试(可注入任意测试时间)。
3.3 Rules的实战陷阱:三条血泪教训
陷阱一:规则爆炸(Rule Explosion)
当Rule数量超过200条,维护成本指数级上升。我们曾在一个客服Agent中积累387条Rule,导致:
- 新增一条“节假日问候语”Rule,需手动检查是否与现有127条“用户情绪识别”Rule冲突;
- 每次发布前需全量回归测试,耗时47分钟。
解法:引入Rule分组与继承
# 定义基础组(base_rules)
base_rules:
- id: "greet_new_user"
when: "user.first_login == true"
then: "send_message('欢迎加入!')"
# 继承并扩展(group: vip_rules)
vip_rules:
extends: base_rules
rules:
- id: "vip_greet"
when: "user.vip_level >= 3"
then: "send_message('尊贵的VIP,您好!')"
所有Agent实例只加载自己所属的Rule组(如 customer_service_vip ),基础组自动继承,避免重复定义。
陷阱二:条件歧义(Ambiguous Condition)
user.age > 18 看似清晰,但当 user.age 是字符串 "18" 或空值 None 时,行为不可预测。
解法:强制类型安全与空值处理
- Rule DSL中所有字段访问必须声明类型:
user.age:int > 18; - 引擎内置空值安全操作符:
user.profile?.address?.city == "Shanghai"(?.表示安全导航); - 单元测试强制覆盖
null、""、[]等边界值。
陷阱三:动作副作用(Action Side Effect)
then: "send_notification('order_confirmed')" 看似无害,但如果该通知服务宕机,整个Rule执行失败,可能导致Agent卡死。
解法:动作解耦与异步化
- 所有
then动作只生成标准化事件(如{"type": "notification.send", "payload": {...}}); - 事件由独立的Event Bus分发,通知服务作为消费者异步处理;
- Rule引擎只关心事件是否成功发出,不等待执行结果。
4. Skill与Rules的协同机制:让Agent真正“懂规矩地干活”
4.1 协同不是调用关系,而是契约驱动的事件流
Skill和Rules之间不存在直接调用。它们通过标准化事件总线(Event Bus)松耦合协作,形成清晰的数据流:
User Request
↓ (Agent Runtime解析)
Intent Detection → [Event: intent_detected {intent: "book_flight", entities: {...}}]
↓
Rules Engine 匹配 → [Rule: "flight_booking_flow" triggers]
↓ (生成执行计划)
Execution Planner → [Plan: [{"skill": "SearchFlightsSkill", "input": {...}}, {"skill": "ValidatePassportSkill", "input": {...}}]]
↓
Skill Executor → 并行调用Skills,捕获结果
↓
Rules Engine 再匹配 → [根据Skill结果触发后续Rule,如"passport_valid == false" → "show_passport_help"]
↓
Response Builder → 合成最终响应
这个流程的关键在于: Rules不决定“用哪个Skill”,而是决定“在什么条件下生成什么执行计划” 。执行计划本身是数据结构,Skill Executor只负责按计划执行,不关心计划为何生成。
我们曾用消息队列(Kafka)实现此总线,但因消息堆积导致延迟。现改用内存级Event Bus(基于Rust编写的 event-bus crate),吞吐量提升12倍,P99延迟<5ms。
4.2 如何设计一条高质量Rule:从需求到上线的六步法
以“防止客服Agent泄露用户手机号”为例,展示完整落地流程:
步骤1:需求抽象
原始需求:“客服回复中不能出现用户手机号”。抽象为Rule目标: 拦截所有包含手机号的响应内容 。
步骤2:定义作用域与优先级
- Scope:
agent:customer_service(仅客服Agent) - Priority:
999(最高优先级,确保在响应生成后立即拦截)
步骤3:编写条件表达式
// 使用正则+语义识别双保险
response.content matches /1[3-9]\d{9}/
or response.content contains any(user.phone_numbers)
步骤4:定义安全动作
then mask_phone_numbers(response.content)
// mask_phone_numbers是预置安全Skill,将手机号替换为**** **** ****
步骤5:本地验证
- 准备测试用例:
"您的订单号12345,手机号13812345678已确认"→ 应返回"您的订单号12345,手机号**** **** ****已确认" - 用Rule引擎CLI工具验证:
rule-engine test --rule-id phone_mask --input test.json
步骤6:灰度发布与监控
- 先对1%客服会话启用,监控
rule_matched_total{rule="phone_mask"}指标; - 设置告警:若匹配率>5%,说明误伤正常文本,需优化正则;
- 72小时无异常后,全量发布。
注意:Rule上线后,必须同步更新对应Skill的文档。例如
mask_phone_numbersSkill的README中,要注明“此Skill被Rule 'phone_mask' 调用,用于GDPR合规”。
4.3 故障排查全景图:当Agent行为异常时,如何快速定位是Skill还是Rules的问题
我们建立了一套标准化排查流程,所有SRE都必须按此执行:
| 现象 | 第一步检查 | 第二步检查 | 根本原因示例 |
|---|---|---|---|
| Agent完全无响应 | 查 agent_runtime 日志中的 execution_timeout | 检查Skill的 timeout_ms 设置是否过小 | SearchFlightsSkill 超时设为100ms,但实际API平均耗时800ms |
| Agent返回错误结果(如错别字) | 查 skill_invocations_total{status="error"} 指标 | 检查Skill的输入契约是否被违反 | 上游传入 city_name: null ,但Skill未校验必填字段 |
| Agent行为不符合预期(如该优惠没生效) | 查 rule_matched_total{rule="vip_discount"} 是否为0 | 检查Rule条件中的 user.vip_level 字段是否在上下文中存在 | 用户Profile服务故障, user.vip_level 未注入到context |
| Agent响应延迟高 | 查 rule_evaluation_duration_seconds P95 | 检查Rule条件是否含高开销操作(如正则回溯) | response.content matches /.*password.*/i 导致O(n²)回溯 |
最关键的证据链是 Trace ID贯穿全程 :从用户请求开始,到Rules匹配、Skill调用、响应生成,所有日志共享同一 trace_id 。用Jaeger查看完整链路,一眼就能看出卡点在哪一层。
5. 工程落地 checklist:你的Agent系统达标了吗?
最后分享一份我们在所有Agent项目上线前强制执行的Checklist。少一项,就不允许发布:
-
[ ] Skill层面
- 所有Skill继承
BaseSkill,且execute()方法无if/else业务逻辑 - 每个Skill的
skill.yaml包含完整契约:input_schema、output_schema、timeout_ms、retries - Skill代码中无硬编码配置(如API Key、Endpoint),全部通过
config.py或环境变量注入
- 所有Skill继承
-
[ ] Rules层面
- 所有Rule存于
rules/目录,按scope/分组(如rules/global/,rules/agent_sales/) - 每条Rule DSL包含
id、when、then、priority、valid_from字段,无缺失 - Rule条件中无硬编码时间、无未声明的变量访问(如
user.age必须写为user.age:int)
- 所有Rule存于
-
[ ] 协同层面
- Agent Runtime中
ExecutionPlanner模块与RulesEngine模块完全解耦,通过EventBus通信 - 所有Skill调用日志包含
trace_id和rule_id(若由Rule触发) -
rule_matched_total和skill_invocations_total指标已接入Grafana看板
- Agent Runtime中
-
[ ] 运维层面
- Rule热更新API已上线,支持
POST /api/v1/rules/hot-reload上传新Rule DSL - Skill Registry提供
GET /skills/active接口,返回所有可用Skill列表及健康状态 - 建立Rule变更审批流:GitHub PR → 自动CI验证 → SRE人工审核 → 生产环境发布
- Rule热更新API已上线,支持
这份Checklist不是纸面文章。我们把它做成了自动化脚本,每次CI构建时运行 ./check-agent-health.sh ,任何一项失败即阻断发布。上线半年来,因Skill/Rules问题导致的P1事故为零。
我在实际项目中发现,团队对Skill和Rules的区分意识,往往比技术选型更重要。一个坚持“Skill只干活、Rules管规矩”的团队,即使用最简陋的YAML Rule引擎,也能做出稳定可靠的Agent;而一个混淆二者边界的团队,哪怕用上最先进的LLM推理框架,也会在三个月后陷入维护泥潭。真正的工程能力,不在炫技,而在对边界感的敬畏。

105

被折叠的 条评论
为什么被折叠?



