☰
WeClaw_63_确定性约束引擎:从Prompt引导到代码级强制的范式转变
2026/10/7 1:10:03 网站建设 项目流程

👋 Hi,带娃的我热爱AI 大模型应用落地、意识解码与 AI 开发工具链。 💡 创业路上,用技术换时间,一起把 AI 变成生产力 🚀 >


WeClaw_63_确定性约束引擎:从Prompt引导到代码级强制的范式转变

第四季系列文章第 2 篇(总第 63 篇)- Constraint Engine · 确定性校验 · 语义级约束 · Hook 注入 · 零 token 成本


📚 专栏信息

《从零到一构建跨平台 AI 助手:WeClaw 实战指南》专栏 ·第四季

专栏定位:面向开发者和技术决策者的实战专栏,用真实案例和完整代码带你理解如何构建生产级 AI 应用

本文深入探讨 Harness Engineering 七大差距中的 G1——架构约束自动执行。我们将从"为什么 Prompt 引导不够用"出发,设计一个确定性约束引擎(ConstraintEngine),在工具执行前进行语义级校验,并将这一过程以零 token 成本嵌入 ReAct 循环。


👨‍💻 作者与项目

作者简介:翁勇刚 WENG YONGGANG
新概念龙虾-WeClaw 开发团队负责人,一群专注于跨平台 AI 应用的实践者
理念:“再复杂的技术,也能用代码讲清楚”

  • 💻 项目地址:https://github.com/wyg5208/weclaw.git
  • 🌐 官网地址:https://weclaw.link
  • 📝 作者 CSDN:https://blog.csdn.net/yweng18
  • ⭐ 欢迎 Star⭐、Fork🍴、贡献代码🤝

📝 摘要

本文结构概览:
从 Agent 执行中的"约束失灵"现象出发——模型明明在 System Prompt 中被告知"不要直接调用底层 shell 执行危险命令",却仍然反复违反——分析 Prompt 引导的局限性,然后设计 ConstraintEngine:一个在工具执行前进行语义级确定性校验的规则引擎。重点讲解如何通过 Hook 注入嵌入 ReAct 循环、如何与现有的 ToolCallValidator 和 _validate_tool_relevance 协同工作,以及如何在前置基础设施修复(M0)的基础上实现零 token 成本的约束执行。

核心问题:
当你告诉 LLM “请在file工具中指定合法路径”,它有时候听,有时候不听。Prompt 是"建议",不是"约束"。如何将关键约束从"建议"升级为"强制"?

关键成果:

  • 设计了 ConstraintEngine 的核心数据结构和规则集
  • 明确了 ToolCallValidator → _validate_tool_relevance → ConstraintEngine 的三层校验链
  • 通过 Hook 注入实现双路径对称覆盖,零 token 成本

适合读者:AI Agent 开发者、对 LLM 约束控制感兴趣的工程师

阅读时长:约 15 分钟

关键词:ConstraintEngine、确定性约束、Hook 注入、ReAct 循环、语义校验、零 token 成本


一、问题现场 —— Prompt 引导的失灵时刻

1.1 一个典型的"约束失灵"场景

用户要求 Agent 修改一个配置文件。Agent 的执行序列如下:

Step 1: file.read("config/default.toml") ✅ 合理 Step 2: file.write("config/default.toml", ...) ✅ 合理 Step 3: file.write("config/default.toml", ...) ⚠️ 重复编辑同一文件 Step 4: file.write("config/default.toml", ...) ⚠️ 又一次 Step 5: file.write("config/default.toml", ...) ❌ 第5次了! Step 6: shell.execute("rm temp/*") ❌ 危险命令!

在 System Prompt 中,我们明确告诉了模型:“不要重复编辑同一文件超过 3 次”、“shell 工具禁止执行 rm 命令”。但模型还是做了。

1.2 为什么 Prompt 引导不够?

Prompt 引导有三个根本性局限:

局限说明类比
概率性遵守LLM 会"理解"约束,但不保证100%执行告诉孩子"不要碰热 stove",但孩子有时还是会碰
上下文稀释长对话中,早期约束被后续上下文"淹没"第50条消息时,第1条的约束已经"模糊"
不可审计约束被违反时没有拦截记录出了事才知道,没有提前预警

核心洞察:Prompt 是给模型看的"说明书",不是给系统执行的"交通规则"。说明书可以被忽略,交通规则不行。

1.3 现有校验器的不足

WeClaw 已有两层校验,但它们都不够:

工具调用请求 │ ▼ ToolCallValidator ← 只检查数量(max_per_call=3) │ PASS ▼ _validate_tool_relevance ← 只检查工具与意图的相关性 │ PASS ▼ 执行工具 ← ⚠️ 这里有语义级空白!

ToolCallValidator只管"调了多少个",_validate_tool_relevance只管"这个工具跟意图有没有关系"。但参数是否安全、依赖层级是否正确、是否重复编辑同一文件——这些语义级约束无人检查。


二、设计 ConstraintEngine —— 确定性约束规则引擎

2.1 核心数据结构

@dataclassclassConstraintRule:"""一条确定性约束规则。"""name:str# 规则名称severity:str# "REJECT" | "WARN"check_fn:Callable[[ToolCallContext],bool]# 校验函数message_template:str# 告警/拒绝消息模板@dataclassclassToolCallContext:"""工具调用的完整上下文,供规则函数使用。"""tool_name:straction_name:strarguments:dictcurrent_intent:strrecent_calls:list[tuple[str,str,str]]# 最近N次调用记录session_step:int# 当前步骤序号@dataclassclassConstraintResult:"""约束校验结果。"""status:str# "PASS" | "REJECT" | "WARN"message:str# 告警/拒绝原因rule_name:str# 触发的规则名

2.2 规则引擎核心

classConstraintEngine:"""确定性约束规则引擎。在工具执行前由 ReAct 循环直接调用。"""def__init__(self):self._rules:list[ConstraintRule]=[]defvalidate(self,ctx:ToolCallContext)->ConstraintResult:"""逐条规则校验,返回第一个 REJECT 或最后一个 WARN。"""last_warn=Noneforruleinself._rules:ifrule.check_fn(ctx):ifrule.severity=="REJECT":returnConstraintResult(status="REJECT",message=rule.message_template.format(**ctx.__dict__),rule_name=rule.name,)elifrule.severity=="WARN"andnotlast_warn:last_warn=ConstraintResult(status="WARN",message=rule.message_template.format(**ctx.__dict__),rule_name=rule.name,)iflast_warn:returnlast_warnreturnConstraintResult(status="PASS",message="",rule_name="")

设计要点:

  • REJECT 优先:遇到第一个 REJECT 立即返回,不继续检查
  • WARN 不阻断:WARN 只记录告警,不阻止执行
  • 零 token 成本:全部是 Python 规则函数,不调用 LLM

2.3 初始规则集

四条规则覆盖最常见的约束失灵场景:

def_build_default_rules()->list[ConstraintRule]:return[# 规则1:依赖层级检查# 禁止低层工具直接调用高层 API(如 shell 直接调用模型 API)ConstraintRule(name="dependency_layer_check",severity="REJECT",check_fn=lambdactx:(ctx.tool_name=="shell"andany(kwinstr(ctx.arguments.get("command",""))forkwin["model_registry","agent_pool"])),message_template="禁止 shell 工具直接操作内部组件: {tool_name}",),# 规则2:输出格式检查# 文件生成类工具必须包含合法路径参数ConstraintRule(name="output_format_check",severity="REJECT",check_fn=lambdactx:(ctx.action_namein("write","create","save")andnotctx.arguments.get("path","")),message_template="文件操作缺少合法 path 参数: {action_name}",),# 规则3:参数安全检查# shell 工具禁止危险命令模式ConstraintRule(name="parameter_safety_check",severity="REJECT",check_fn=lambdactx:(ctx.tool_name=="shell"and_has_dangerous_pattern(ctx.arguments.get("command",""))),message_template="shell 命令包含危险模式: {tool_name}",),# 规则4:单任务文件编辑次数上限ConstraintRule(name="max_file_edits_per_task",severity="WARN",check_fn=lambdactx:(ctx.action_namein("write","edit")andsum(1forcinctx.recent_callsifc[0]=="file"andc[1]in("write","edit"))>=20),message_template="单任务文件编辑已达上限({session_step}步),请考虑合并操作",),]

为什么 max_file_edits 是 WARN 而不是 REJECT?因为重复编辑可能是合理的(比如增量修改一个大文件),我们只想提醒模型注意,而不是强制阻断。


三、三层校验链 —— 与现有系统的协同

3.1 校验链全景

工具调用请求 │ ▼ ① ToolCallValidator ← 数量限制(max_per_call=3) │ PASS ▼ ② _validate_tool_relevance ← 意图相关性(file工具 vs casual_chat意图) │ PASS ▼ ③ ConstraintEngine ← 语义级约束(本篇新增) │ PASS ▼ 执行工具

三层校验各有职责,互不重叠:

层级校验对象例子阻断方式
ToolCallValidator数量“一次调了5个工具”REJECT
_validate_tool_relevance意图相关性“casual_chat 意图调了 shell”REJECT
ConstraintEngine语义正确性“shell 执行了 rm”REJECT 或 WARN

3.2 职责边界

一个常见的设计陷阱是让 ConstraintEngine “什么都管”——这样会导致它变成一个上帝类。我们严格限定其边界:

  • ✅管:参数安全、依赖层级、输出格式、重复模式
  • ❌不管:工具数量(ToolCallValidator 的职责)、意图匹配(_validate_tool_relevance 的职责)

四、前置修复 —— M0 阻塞项解除

4.1 为什么需要先修 ExecutionTracker?

ConstraintEngine 的max_file_edits_per_task规则需要查询"最近 N 次调用记录"。这些记录存储在ExecutionTracker.recent_success_calls字段中——但代码级评审发现,这个字段没有公开的访问方法。

# ExecutionTracker 现有代码(agent.py L77-178)classExecutionTracker:def__init__(self):self.recent_success_calls:list[tuple[str,str,str]]=[]# ...defrecord_success(self,tool_name,action_name,args_hash):self.recent_success_calls.append((tool_name,action_name,args_hash))# ...# ❌ 没有 get_recent_calls() 方法!

4.2 修复:3 行代码

defget_recent_calls(self,n:int=3)->list[tuple[str,str,str]]:"""获取最近 N 次成功调用记录 (tool_name, action_name, args_hash)。"""returnself.recent_success_calls[-n:]

看起来微不足道——3 行代码,但它是一个阻塞项:ConstraintEngine 依赖此方法,没有它就无法获取上下文。

4.3 另一个阻塞项:chat_stream 路径遗漏

代码级评审还发现,chat_stream(流式路径)完全没有调用_validate_tool_relevance。这意味着流式模式下,模型可以调用任何工具——即使与当前意图完全无关。

# _chat_impl 路径(非流式)— 有校验 ✅is_relevant,reject_reason=self._validate_tool_relevance(tool_name,action_name)ifnotis_relevant:# ... 拒绝并 continue# chat_stream 路径(流式)— 无校验 ❌# 直接执行工具,没有任何前置校验

修复:在chat_stream的工具执行循环中补全_validate_tool_relevance调用(约10行),使两条路径对称。

关键洞察:这种"双路径不对称"是大型代码库中最隐蔽的 Bug 类型。非流式路径有完整校验,流式路径却遗漏了——而流式是桌面端和 PWA 的主要执行路径。


五、Hook 注入 —— 嵌入 ReAct 循环

5.1 抽取私有方法

所有 Hook 注入点都抽取为私有方法,供_chat_impl和chat_stream双路径复用:

def_check_constraint(self,tool_name,action_name,arguments,tc_id,_batch,step_idx,intent_result):"""ConstraintEngine 前置检查(G1),返回 True 表示被拦截应 continue。"""ifnotself._constraint_engine:returnFalse# 未启用,零开销result=self._constraint_engine.validate(ToolCallContext(tool_name=tool_name,action_name=action_name,arguments=arguments,current_intent=intent_result.primary_intent,recent_calls=self._execution_tracker.get_recent_calls(3),session_step=step_idx,))ifresult.status=="REJECT":_batch.append(("tool",result.message,{"tool_call_id":tc_id}))returnTrue# 被拦截,调用方应 continueifresult.status=="WARN":logger.warning("ConstraintEngine WARN: %s (rule=%s)",result.message,result.rule_name)returnFalse# 放行

5.2 注入位置

在 ReAct 循环中,ConstraintEngine 检查位于_validate_tool_relevance之后、工具执行之前:

# ReAct 循环中的工具处理(两条路径共用模式)# ① 数量校验validation=self.tool_validator.validate(tool_calls)ifvalidation.status=="REJECT":continue# ② 意图相关性校验is_relevant,reject_reason=self._validate_tool_relevance(tool_name,action_name)ifnotis_relevant:continue# ③ 语义约束校验(新增 G1)ifself._check_constraint(tool_name,action_name,arguments,tc_entry["id"],_batch,step_idx,intent_result):continue# 被约束引擎拦截# ④ 执行工具result=awaitself.tool_registry.call_function(...)

5.3 零开销保证

当 ConstraintEngine 未启用时,_check_constraint的第一行就返回False——没有任何额外计算。这保证了对现有系统的性能零影响。


六、配置驱动灰度

6.1 配置结构

# config/default.toml — 添加在 [agent.trace] 之后 [agent.constraint_engine] enabled = false # 默认关闭 max_file_edits_per_task = 20 # 文件编辑上限 enable_dependency_check = true # 依赖层级检查 enable_safety_check = true # 参数安全检查

6.2 灰度策略

# gui_app.py _initialize_core_components() 中constraint_config=agent_config.get("constraint_engine",{})ifconstraint_config.get("enabled",False):engine=ConstraintEngine()engine.load_rules_from_config(constraint_config)self._agent._constraint_engine=engine logger.info("ConstraintEngine 已启用,加载 %d 条规则",len(engine._rules))

灰度路径:

  1. 第一周:仅启用parameter_safety_check(最安全,拦截危险命令)
  2. 第二周:追加output_format_check(防止文件操作缺少路径)
  3. 第三周:追加dependency_layer_check(防止跨层调用)
  4. 第四周:追加max_file_edits_per_task(WARN 模式,观察误报率)

七、与现有安全体系的协同

WeClaw 已有完善的安全体系,ConstraintEngine 不是替代,而是补充:

┌─────────────────┐ │ Prompt Security │ ← 输入侧:检测恶意提示注入 └────────┬────────┘ │ ┌────────▼────────┐ │ Permission Manager│ ← 权限侧:三级风险分类 └────────┬────────┘ │ ┌────────▼────────┐ │ Audit Logger │ ← 审计侧:全量记录 └────────┬────────┘ │ ┌───────────────────────┼───────────────────────┐ │ │ │ ┌────▼─────┐ ┌──────▼──────┐ ┌──────▼──────┐ │ToolCall │ │_validate_ │ │Constraint │ │Validator │ │tool_relevance│ │Engine(新增) │ │(数量) │ │(意图匹配) │ │(语义约束) │ └──────────┘ └─────────────┘ └─────────────┘

ConstraintEngine 的独特价值:它是唯一能访问recent_calls上下文的校验器。其他校验器只看"当前这一次调用",而 ConstraintEngine 能看到"最近 N 次调用"——这使得重复模式检测成为可能。


八、核心教训

8.1 约束分两种:建议性和强制性

Prompt 是"建议性约束"——模型理解但不保证遵守。代码是"强制性约束"——绕不过去。关键约束必须两层都有:Prompt 告诉模型"为什么"不应该做,ConstraintEngine 确保它"做不到"。

8.2 前置修复的价值

M0 阶段只改了 13 行代码(3+10),但它们是整个方案的基石。如果没有先修get_recent_calls(),ConstraintEngine 的核心规则就无法工作。最小阻塞项往往是最容易被忽视的。

8.3 WARN 比 REJECT 更难设计

REJECT 是非黑即白的——违反了就拒绝。WARN 是微妙的——什么时候该提醒?提醒太频繁是噪音,太稀疏是失职。我们选择max_file_edits_per_task作为唯一 WARN 规则,正是因为它是"建议但不强制"的典型场景。


📖 相关文章

  • WeClaw_62_Harness工程全景:六层模型评估与WeClaw 4.3分成熟度诊断
  • WeClaw_60_能力越强护栏越硬:AI自主进化的安全边界工程与核能类比

本文是 WeClaw 专栏第四季的第 2 篇(总第 63 篇)。如果这篇文章对你有帮助,欢迎给项目点个 Star ⭐

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询