第5章-机械化架构约束《智驭》
第5章 机械化架构约束 — 让 AI Agent 在规则的铁轨上奔跑
核心命题:提示词是"建议",约束是"法律"。当 AI Agent 的输出必须穿越一道由机器验证的规则之墙时,架构退化不再是概率事件,而是物理上不可能发生的事。机械化架构约束(Mechanized Architectural Constraints)是 Harness Engineering 五大核心组件中最具确定性的一环——它将人类的架构智慧编码为可执行的规则,让 Agent 在规则的铁轨上全速奔跑,而永不脱轨。
本章公式:
Constrained Agent = Agent ∩ Rules(TypeSystem ∩ SchemaValidator ∩ LintEngine ∩ PolicyGuard)
控制论视角:机械化约束本质上是一个"前馈控制器"(Feedforward Controller)——它在 Agent 行动之前就预设了可行空间,将偏差消灭在萌芽状态。这与经典控制论中"反馈控制"(事后修正)形成互补,二者共同构成完整的控制回路。
5.1 引言:架构退化的隐形杀手
5.1.1 一个真实的"温水煮青蛙"案例
2025年6月,一家 SaaS 公司的工程团队经历了一场悄无声息的架构灾难。
这家公司开发一个企业级项目管理平台,团队在年初引入了 AI Agent 辅助开发。最初效果令人惊喜——Agent 的代码生成速度快、质量看起来也不错。团队负责人在季度汇报中骄傲地宣布:“AI 让我们的开发效率提升了 40%。”
但三个月后,问题开始浮现。
一个看似简单的功能需求——“在任务详情页添加一个评论功能”——触发了连锁反应:
- Agent 为新功能创建了一个
CommentService,但把它放在了src/services/目录下,而非按架构约定应放在src/modules/comments/目录下 CommentService直接依赖了TaskRepository,违反了"模块间通过接口通信"的架构规则- 评论的权限校验逻辑与任务的权限校验逻辑重复实现,但行为略有不同
- 新增的 API 路由使用了 REST 风格(
/api/comments),而系统其他部分使用 GraphQL - 错误处理采用了直接抛出异常的方式,而系统约定使用
Result<T, E>模式
这些问题中的每一个单独看都不是致命的。 代码能跑,功能能用,测试能过。但它们合在一起,就像在一座精心设计的哥特式大教堂旁边,突然冒出了一间用预制板搭建的临时工棚。
三个月后,当团队试图做一次大规模的依赖升级时,灾难爆发了:
| 问题类型 | 数量 | 根因 |
|---|---|---|
| 模块间循环依赖 | 23 处 | Agent 不了解分层架构约束 |
| 不一致的错误处理 | 47 处 | Agent 有时用 Result,有时用异常,有时用 null |
| 数据模型重复定义 | 12 处 | Agent 不知道已有类型定义在哪里 |
| 跨层直接调用 | 31 处 | Agent 为"效率"绕过了 Service 层 |
| 不一致的 API 风格 | 18 处 | Agent 每次生成时选择不同风格 |
| 安全策略违反 | 9 处 | Agent 在 Controller 层直接操作了数据库 |
总计 140 处架构违规。 团队花了两周时间才全部修复,而这两周本可以用来开发新功能。
更令人沮丧的是,这些问题本不应该发生。公司的架构文档清楚地写着分层规则、命名约定、错误处理策略。但 AI Agent 不会"阅读"架构文档——它只是根据上下文窗口中的信息来生成代码。
5.1.2 为什么 Prompt 中的架构约束不可靠
很多开发者的第一反应是:“那我可以在 Prompt 里告诉 Agent 这些规则啊。”
是的,你可以。但这种方法有三个致命缺陷:
缺陷一:上下文窗口的稀缺性
一个完整的架构约束集合通常有 50-100 条规则,总计 5000-10000 个 Token。这些 Token 会与代码上下文、需求描述、对话历史竞争有限的窗口空间。当窗口接近饱和时,系统会自动压缩上下文——而架构约束往往是最先被压缩掉的部分。
缺陷二:自然语言的模糊性
即使架构约束被完整地包含在 Prompt 中,自然语言的描述方式也会导致歧义:
❌ 自然语言约束(模糊):
"Service 层不应该直接依赖其他模块的 Repository"
AI Agent 的理解可能是:
1. Service 层不能 import 其他模块的 Repository 文件 ✓
2. Service 层不能直接调用 Repository 的方法,但可以通过 DI 注入 ✗
3. 只有同模块的 Service 才能访问自己的 Repository ✗
4. 以上都不对,只是说 Repository 的实例化要由上层负责 ✗✗
同一条规则,四种理解,每种都有道理。自然语言的灵活性在这里成了敌人。
缺陷三:缺乏反馈闭环
即使 Agent 理解了约束,Prompt 中也没有机制告诉它"你刚才违反了这条规则"。Agent 生成代码后就"忘记"了约束的存在。没有检测,没有反馈,没有修正。
Prompt 约束 → Agent 生成代码 → 代码违反约束 → 无反馈 → Agent 下次继续违反
↑
这里缺少一个"检测器"
5.1.3 机械化约束的哲学:让错误"不可能"发生
让我们用一个类比来理解机械化约束的力量。
类比:铁路 vs 公路
公路上的汽车可以自由行驶,但也可以闯红灯、逆行、超速。交通安全依赖于驾驶员的"自觉遵守规则"。即使有交通摄像头(反馈控制),违规行为也是先发生、后被发现。
铁路上的列车则完全不同。铁轨本身就是一种机械化约束——列车物理上不可能偏离轨道。道岔(switch)决定了列车的可行路径。信号系统(signal system)在列车进入区间前就锁定了可行路线。
机械化约束的核心原则:
| 原则 | 说明 | 示例 |
|---|---|---|
| 不可绕过性 | 约束必须在代码路径上强制执行,而非建议 | TypeScript 编译器不允许类型错误通过 |
| 即时反馈 | 违规必须在生成后立即被检测到 | ESLint 在保存时即时报告问题 |
| 机器可验证 | 规则必须形式化,不留主观判断空间 | “函数长度 ≤ 50 行"而非"函数要简短” |
| 可组合性 | 多条规则可以并行执行,互不冲突 | TypeCheck + Lint + Policy 独立运行 |
| 渐进式严格 | 新项目可以从宽松规则开始,逐步收紧 | 先用 warn 模式收集基线,再切换为 error |
在接下来的章节中,我们将深入探讨如何构建这样一个机械化约束系统。它不是替代 Prompt 中的架构指导,而是作为 Prompt 的"执法部门"——Prompt 负责"立法"(描述期望),约束引擎负责"执法"(验证执行)。
5.2 约束的理论基础
5.2.1 约束的分类学:四层防线
在 Harness Engineering 的框架中,机械化约束按照执行时机和严格程度分为四个层次,形成纵深防御体系:
各层约束的特点对比:
| 层次 | 执行时机 | 反馈速度 | 严格程度 | 绕过难度 | 典型工具 |
|---|---|---|---|---|---|
| 编译时 | 代码编写/保存时 | < 1秒 | 极高(编译失败) | 极高 | TypeScript, Mypy, Rust |
| 静态分析 | 保存/提交时 | 1-5秒 | 高(CI 门禁) | 中(可 eslint-disable) | ESLint, Pylint, SonarQube |
| 架构规则 | 提交/合并时 | 5-30秒 | 高(PR 阻断) | 低(需要架构评审豁免) | ArchUnit, DependencyCruiser |
| 运行时 | 请求处理时 | 毫秒级 | 中(请求拒绝) | 极高(无法绕过) | Zod, Joi, Casbin |
5.2.2 约束的形式化表达
要让约束"机器可验证",首先需要将自然语言描述的规则转化为形式化表达。一个完整的约束定义包含五个要素:
Constraint = {
id: 唯一标识符(用于追踪和报告)
scope: 作用域(文件/模块/系统级别)
predicate: 判定条件(布尔函数,输入为代码 AST 或运行时状态)
severity: 严重级别(error / warning / info)
remediation: 修复建议(指导 Agent 如何修正)
}
示例:将自然语言约束形式化
自然语言:“Controller 层不能直接调用 Repository 层,必须通过 Service 层”
形式化表达:
const controllerNoDirectRepoAccess: Constraint = {
id: "ARCH-001",
scope: "file",
predicate: (file) => {
if (!file.path.includes("/controllers/")) return true; // 只检查 Controller 文件
const imports = file.getImports();
return !imports.some(imp => imp.source.includes("/repositories/"));
},
severity: "error",
remediation: "Controller 应通过 Service 层间接访问 Repository。请注入对应的 Service 实例。"
};
自然语言:“所有 API 端点必须有输入验证”
形式化表达:
const apiInputValidation: Constraint = {
id: "SEC-001",
scope: "file",
predicate: (file) => {
const routes = file.getRouteHandlers();
return routes.every(route => {
const params = route.getParameters();
return params.every(param => param.hasSchemaValidation());
});
},
severity: "error",
remediation: "所有 API 端点的请求参数必须通过 Zod Schema 或 Joi Schema 进行验证。"
};
5.2.3 约束的完备性与一致性
在设计约束系统时,必须关注两个理论属性:
完备性(Completeness):约束集合是否覆盖了所有需要防护的架构决策?
一致性(Consistency):约束集合中是否存在相互矛盾的规则?
在实际工程中,追求"绝对完备"是不现实的(哥德尔不完备定理的启示),但应该追求"足够完备"——覆盖团队最关心的架构决策。
实践建议:从团队过去最常犯的架构违规开始,逐步扩展约束集合。每次发现新的违规模式,就将其编码为一条新规则。
5.3 约束引擎核心实现(TypeScript)
5.3.1 整体架构设计
约束引擎是整个机械化约束系统的核心。它接收 AI Agent 生成的代码作为输入,运行一组可配置的规则,输出验证结果和修复建议。
5.3.2 核心类型定义
首先定义约束系统的基础类型:
// === constraint-types.ts ===
/**
* 约束严重级别
* - error: 阻断性问题,必须修复
* - warning: 建议性问题,应该修复
* - info: 信息性提示,可以考虑修复
*/
export type Severity = "error" | "warning" | "info";
/**
* 约束作用域
* - file: 单文件级别检查
* - module: 模块级别检查(跨文件)
* - project: 项目级别检查(全局架构)
*/
export type Scope = "file" | "module" | "project";
/**
* 违规位置
*/
export interface ViolationLocation {
filePath: string;
line: number;
column: number;
endLine?: number;
endColumn?: number;
}
/**
* 单条违规记录
*/
export interface Violation {
ruleId: string;
severity: Severity;
message: string;
location: ViolationLocation;
remediation: string;
context?: Record<string, unknown>;
}
/**
* 代码分析上下文 —— 从 AST 和文件元数据中提取
*/
export interface AnalysisContext {
/** 当前文件路径 */
filePath: string;
/** 文件内容 */
content: string;
/** 导入列表 */
imports: ImportInfo[];
/** 导出列表 */
exports: ExportInfo[];
/** 函数/方法定义列表 */
functions: FunctionInfo[];
/** 类定义列表 */
classes: ClassInfo[];
/** 类型定义列表 */
types: TypeInfo[];
/** 整个项目的文件列表(用于 project 级别检查) */
allFiles?: string[];
/** 项目的依赖图(用于循环依赖检测) */
dependencyGraph?: Map<string, string[]>;
}
export interface ImportInfo {
source: string; // import 来源
specifiers: string[]; // 导入的具体名称
isTypeOnly: boolean; // 是否仅导入类型
line: number;
}
export interface ExportInfo {
name: string;
kind: "function" | "class" | "interface" | "type" | "const" | "enum";
line: number;
}
export interface FunctionInfo {
name: string;
line: number;
endLine: number;
parameterCount: number;
returnType: string | null;
hasJSDoc: boolean;
body: string;
complexity: number; // 圈复杂度
maxNestingDepth: number; // 最大嵌套深度
}
export interface ClassInfo {
name: string;
line: number;
endLine: number;
methods: FunctionInfo[];
properties: { name: string; type: string | null; visibility: "public" | "private" | "protected" }[];
implements: string[];
extends: string | null;
}
export interface TypeInfo {
name: string;
kind: "interface" | "type" | "enum";
line: number;
hasAnyType: boolean;
}
/**
* 约束规则接口
*/
export interface ConstraintRule {
/** 规则唯一 ID */
id: string;
/** 规则名称 */
name: string;
/** 规则描述 */
description: string;
/** 严重级别 */
severity: Severity;
/** 作用域 */
scope: Scope;
/** 检查函数:返回违规列表(空数组表示通过) */
check: (context: AnalysisContext) => Violation[];
}
/**
* 验证报告
*/
export interface ValidationReport {
/** 检查时间戳 */
timestamp: string;
/** 总规则数 */
totalRules: number;
/** 通过的规则数 */
passedRules: number;
/** 违规总数 */
totalViolations: number;
/** 按严重级别统计 */
violationsBySeverity: { error: number; warning: number; info: number };
/** 所有违规详情 */
violations: Violation[];
/** 检查耗时(毫秒) */
durationMs: number;
/** 是否全部通过 */
passed: boolean;
}
5.3.3 约束引擎实现
// === constraint-engine.ts ===
import { ConstraintRule, AnalysisContext, ValidationReport, Violation, Severity } from "./constraint-types";
/**
* 约束引擎 —— 管理和执行一组约束规则
*
* 设计原则:
* 1. 规则独立执行,互不干扰
* 2. 单条规则失败不影响其他规则
* 3. 支持并行执行以提高性能
* 4. 提供详细的验证报告
*/
export class ConstraintEngine {
private rules: Map<string, ConstraintRule> = new Map();
private enabledRules: Set<string> = new Set();
private ruleHistory: Map<string, { pass: number; fail: number }> = new Map();
/**
* 注册一条约束规则
*/
registerRule(rule: ConstraintRule): void {
if (this.rules.has(rule.id)) {
throw new Error(`Rule "${rule.id}" is already registered. Rule IDs must be unique.`);
}
this.rules.set(rule.id, rule);
this.enabledRules.add(rule.id);
this.ruleHistory.set(rule.id, { pass: 0, fail: 0 });
}
/**
* 批量注册规则
*/
registerRules(rules: ConstraintRule[]): void {
for (const rule of rules) {
this.registerRule(rule);
}
}
/**
* 启用/禁用指定规则
*/
setRuleEnabled(ruleId: string, enabled: boolean): void {
if (!this.rules.has(ruleId)) {
throw new Error(`Rule "${ruleId}" not found.`);
}
if (enabled) {
this.enabledRules.add(ruleId);
} else {
this.enabledRules.delete(ruleId);
}
}
/**
* 获取所有已注册规则
*/
getRules(): ConstraintRule[] {
return Array.from(this.rules.values());
}
/**
* 获取已启用的规则
*/
getEnabledRules(scope?: string): ConstraintRule[] {
return Array.from(this.rules.values()).filter(
rule => this.enabledRules.has(rule.id) && (!scope || rule.scope === scope)
);
}
/**
* 执行所有启用的规则,返回验证报告
*/
async validate(context: AnalysisContext): Promise<ValidationReport> {
const startTime = Date.now();
const enabledRules = this.getEnabledRules();
const allViolations: Violation[] = [];
// 按作用域分组执行
const fileRules = enabledRules.filter(r => r.scope === "file");
const moduleRules = enabledRules.filter(r => r.scope === "module");
const projectRules = enabledRules.filter(r => r.scope === "project");
// 文件级规则:并行执行
const fileResults = await Promise.allSettled(
fileRules.map(rule => this.executeRule(rule, context))
);
// 模块级规则:并行执行
const moduleResults = await Promise.allSettled(
moduleRules.map(rule => this.executeRule(rule, context))
);
// 项目级规则:并行执行
const projectResults = await Promise.allSettled(
projectRules.map(rule => this.executeRule(rule, context))
);
// 收集所有违规
for (const result of [...fileResults, ...moduleResults, ...projectResults]) {
if (result.status === "fulfilled") {
allViolations.push(...result.value);
} else {
// 规则执行本身出错,记录但不阻断
console.error(`Rule execution error: ${result.reason}`);
}
}
// 更新规则历史统计
const failedRuleIds = new Set(allViolations.map(v => v.ruleId));
for (const rule of enabledRules) {
const history = this.ruleHistory.get(rule.id)!;
if (failedRuleIds.has(rule.id)) {
history.fail++;
} else {
history.pass++;
}
}
// 构建报告
const violationsBySeverity: Record<Severity, number> = { error: 0, warning: 0, info: 0 };
for (const v of allViolations) {
violationsBySeverity[v.severity]++;
}
const durationMs = Date.now() - startTime;
return {
timestamp: new Date().toISOString(),
totalRules: enabledRules.length,
passedRules: enabledRules.length - failedRuleIds.size,
totalViolations: allViolations.length,
violationsBySeverity,
violations: allViolations,
durationMs,
passed: violationsBySeverity.error === 0,
};
}
/**
* 执行单条规则(带超时保护)
*/
private async executeRule(rule: ConstraintRule, context: AnalysisContext): Promise<Violation[]> {
const TIMEOUT_MS = 5000; // 单条规则 5 秒超时
return new Promise<Violation[]>((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Rule "${rule.id}" timed out after ${TIMEOUT_MS}ms`));
}, TIMEOUT_MS);
try {
const violations = rule.check(context);
clearTimeout(timer);
resolve(violations);
} catch (error) {
clearTimeout(timer);
reject(error);
}
});
}
/**
* 生成人类可读的反馈消息
*/
formatFeedback(report: ValidationReport): string {
if (report.passed) {
return `✅ 全部 ${report.totalRules} 条规则通过(耗时 ${report.durationMs}ms)`;
}
const lines: string[] = [];
lines.push(`❌ 验证失败:${report.totalViolations} 条违规(${report.violationsBySeverity.error} error / ${report.violationsBySeverity.warning} warning / ${report.violationsBySeverity.info} info)`);
lines.push("");
// 按严重级别排序:error > warning > info
const sorted = [...report.violations].sort((a, b) => {
const order: Record<Severity, number> = { error: 0, warning: 1, info: 2 };
return order[a.severity] - order[b.severity];
});
for (const v of sorted) {
const icon = v.severity === "error" ? "🔴" : v.severity === "warning" ? "🟡" : "🔵";
lines.push(`${icon} [${v.ruleId}] ${v.message}`);
lines.push(` 📍 ${v.location.filePath}:${v.location.line}:${v.location.column}`);
lines.push(` 💡 ${v.remediation}`);
lines.push("");
}
return lines.join("\n");
}
/**
* 获取规则历史统计(用于分析 Agent 的改进趋势)
*/
getRuleHistory(): Map<string, { pass: number; fail: number }> {
return new Map(this.ruleHistory);
}
}
5.3.4 内置规则集:架构约束
// === rules/architecture-rules.ts ===
import { ConstraintRule, AnalysisContext, Violation } from "../constraint-types";
/**
* ARCH-001: 分层架构约束
* Controller → Service → Repository,禁止跨层调用
*/
export const layerDependencyRule: ConstraintRule = {
id: "ARCH-001",
name: "分层架构依赖",
description: "Controller 只能依赖 Service,Service 只能依赖 Repository。禁止跨层或反向依赖。",
severity: "error",
scope: "file",
check: (ctx: AnalysisContext): Violation[] => {
const violations: Violation[] = [];
const filePath = ctx.filePath;
// 定义合法的依赖方向
const layerMap: Record<string, string[]> = {
"/controllers/": ["/services/"], // Controller 只能依赖 Service
"/services/": ["/repositories/", "/utils/"], // Service 可以依赖 Repository 和 Utils
"/repositories/": ["/models/", "/utils/"], // Repository 可以依赖 Model 和 Utils
"/models/": [], // Model 不依赖任何业务层
};
// 确定当前文件所在层
let currentLayer: string | null = null;
for (const [layer] of Object.entries(layerMap)) {
if (filePath.includes(layer)) {
currentLayer = layer;
break;
}
}
if (!currentLayer) return violations; // 不在任何业务层中(如 /utils/)
const allowedLayers = layerMap[currentLayer] || [];
// 检查每个 import 是否违反层级约束
for (const imp of ctx.imports) {
const isFromBusinessLayer = Object.keys(layerMap).some(l => imp.source.includes(l));
if (!isFromBusinessLayer) continue; // 第三方库或工具类不受此规则约束
const isAllowed = allowedLayers.some(l => imp.source.includes(l));
if (!isAllowed) {
violations.push({
ruleId: "ARCH-001",
severity: "error",
message: `分层架构违规:${currentLayer} 层文件不能导入 ${imp.source}`,
location: { filePath, line: imp.line, column: 1 },
remediation: `请通过依赖注入或接口抽象来间接访问。${currentLayer} 层允许依赖的层:${allowedLayers.join(", ") || "无"}`,
});
}
}
return violations;
},
};
/**
* ARCH-002: 循环依赖检测
*/
export const circularDependencyRule: ConstraintRule = {
id: "ARCH-002",
name: "循环依赖检测",
description: "模块之间不允许存在循环依赖",
severity: "error",
scope: "project",
check: (ctx: AnalysisContext): Violation[] => {
const violations: Violation[] = [];
const graph = ctx.dependencyGraph;
if (!graph) return violations;
// 使用 DFS 检测环
const visited = new Set<string>();
const inStack = new Set<string>();
const cycles: string[][] = [];
function dfs(node: string, path: string[]): void {
if (inStack.has(node)) {
// 找到环
const cycleStart = path.indexOf(node);
if (cycleStart >= 0) {
cycles.push([...path.slice(cycleStart), node]);
}
return;
}
if (visited.has(node)) return;
visited.add(node);
inStack.add(node);
path.push(node);
const neighbors = graph.get(node) || [];
for (const neighbor of neighbors) {
dfs(neighbor, [...path]);
}
inStack.delete(node);
}
for (const node of graph.keys()) {
dfs(node, []);
}
for (const cycle of cycles) {
violations.push({
ruleId: "ARCH-002",
severity: "error",
message: `循环依赖:${cycle.join(" → ")}`,
location: { filePath: cycle[0], line: 1, column: 1 },
remediation: "请通过提取公共接口、引入中间层或使用依赖注入来打破循环。",
});
}
return violations;
},
};
/**
* ARCH-003: 模块边界约束
* 模块的私有实现不能从模块外部直接导入
*/
export const moduleBoundaryRule: ConstraintRule = {
id: "ARCH-003",
name: "模块边界约束",
description: "模块的 internal/ 或 private/ 目录下的文件不能从模块外部导入",
severity: "error",
scope: "file",
check: (ctx: AnalysisContext): Violation[] => {
const violations: Violation[] = [];
for (const imp of ctx.imports) {
if (imp.source.includes("/internal/") || imp.source.includes("/private/")) {
// 提取模块根目录
const importerModule = extractModuleName(ctx.filePath);
const importedModule = extractModuleName(imp.source);
if (importerModule !== importedModule) {
violations.push({
ruleId: "ARCH-003",
severity: "error",
message: `模块边界违规:不能从模块 "${importerModule}" 导入 "${importedModule}" 的内部实现`,
location: { filePath: ctx.filePath, line: imp.line, column: 1 },
remediation: `请通过 "${importedModule}" 的公开 API(index.ts 或 public/ 目录)来访问其功能。`,
});
}
}
}
return violations;
},
};
function extractModuleName(filePath: string): string {
const match = filePath.match(/\/modules\/([^/]+)/);
return match ? match[1] : "unknown";
}
5.3.5 内置规则集:代码质量约束
// === rules/quality-rules.ts ===
import { ConstraintRule, AnalysisContext, Violation } from "../constraint-types";
/**
* QUAL-001: 函数复杂度限制
*/
export const functionComplexityRule: ConstraintRule = {
id: "QUAL-001",
name: "函数复杂度限制",
description: "函数的圈复杂度不得超过 10,最大嵌套深度不得超过 4",
severity: "warning",
scope: "file",
check: (ctx: AnalysisContext): Violation[] => {
const violations: Violation[] = [];
const MAX_COMPLEXITY = 10;
const MAX_NESTING = 4;
for (const fn of ctx.functions) {
if (fn.complexity > MAX_COMPLEXITY) {
violations.push({
ruleId: "QUAL-001",
severity: "warning",
message: `函数 "${fn.name}" 的圈复杂度为 ${fn.complexity},超过阈值 ${MAX_COMPLEXITY}`,
location: { filePath: ctx.filePath, line: fn.line, column: 1 },
remediation: "建议将复杂函数拆分为多个职责单一的小函数。考虑使用早返回(early return)模式减少分支嵌套。",
context: { functionName: fn.name, complexity: fn.complexity },
});
}
if (fn.maxNestingDepth > MAX_NESTING) {
violations.push({
ruleId: "QUAL-001",
severity: "warning",
message: `函数 "${fn.name}" 的最大嵌套深度为 ${fn.maxNestingDepth},超过阈值 ${MAX_NESTING}`,
location: { filePath: ctx.filePath, line: fn.line, column: 1 },
remediation: "使用 early return、guard clause 或提取辅助函数来降低嵌套深度。",
});
}
}
return violations;
},
};
/**
* QUAL-002: 函数长度限制
*/
export const functionLengthRule: ConstraintRule = {
id: "QUAL-002",
name: "函数长度限制",
description: "单个函数的代码行数不得超过 50 行(不含空行和注释)",
severity: "warning",
scope: "file",
check: (ctx: AnalysisContext): Violation[] => {
const violations: Violation[] = [];
const MAX_LINES = 50;
for (const fn of ctx.functions) {
const length = fn.endLine - fn.line + 1;
if (length > MAX_LINES) {
violations.push({
ruleId: "QUAL-002",
severity: "warning",
message: `函数 "${fn.name}" 有 ${length} 行,超过阈值 ${MAX_LINES} 行`,
location: { filePath: ctx.filePath, line: fn.line, column: 1, endLine: fn.endLine },
remediation: "将函数拆分为多个小函数。每个函数应该只做一件事,并把它做好。",
});
}
}
return violations;
},
};
/**
* QUAL-003: JSDoc 文档要求
*/
export const jsdocRequiredRule: ConstraintRule = {
id: "QUAL-003",
name: "公开 API 必须有文档",
description: "所有导出的函数和类必须有 JSDoc 文档",
severity: "warning",
scope: "file",
check: (ctx: AnalysisContext): Violation[] => {
const violations: Violation[] = [];
const exportedFunctions = new Set(ctx.exports.filter(e => e.kind === "function").map(e => e.name));
const exportedClasses = new Set(ctx.exports.filter(e => e.kind === "class").map(e => e.name));
for (const fn of ctx.functions) {
if (exportedFunctions.has(fn.name) && !fn.hasJSDoc) {
violations.push({
ruleId: "QUAL-003",
severity: "warning",
message: `导出的函数 "${fn.name}" 缺少 JSDoc 文档`,
location: { filePath: ctx.filePath, line: fn.line, column: 1 },
remediation: "请为导出的函数添加 JSDoc 文档,包括功能描述、参数说明和返回值说明。",
});
}
}
for (const cls of ctx.classes) {
if (exportedClasses.has(cls.name)) {
// 检查类的公开方法是否都有文档
for (const method of cls.methods) {
if (method.name !== "constructor" && !method.hasJSDoc) {
violations.push({
ruleId: "QUAL-003",
severity: "warning",
message: `类 "${cls.name}" 的公开方法 "${method.name}" 缺少 JSDoc 文档`,
location: { filePath: ctx.filePath, line: method.line, column: 1 },
remediation: "请为公开方法添加 JSDoc 文档。",
});
}
}
}
}
return violations;
},
};
/**
* QUAL-004: 禁止使用 any 类型
*/
export const noAnyTypeRule: ConstraintRule = {
id: "QUAL-004",
name: "禁止使用 any 类型",
description: "禁止在类型定义中使用 any,应使用 unknown 或具体类型",
severity: "error",
scope: "file",
check: (ctx: AnalysisContext): Violation[] => {
const violations: Violation[] = [];
for (const type of ctx.types) {
if (type.hasAnyType) {
violations.push({
ruleId: "QUAL-004",
severity: "error",
message: `类型 "${type.name}" 包含 any 类型`,
location: { filePath: ctx.filePath, line: type.line, column: 1 },
remediation: "将 any 替换为 unknown(需要类型收窄)或具体的类型定义。对于第三方库返回值,使用类型断言 + 运行时验证。",
});
}
}
// 额外检查:函数参数和返回值中的 any
for (const fn of ctx.functions) {
if (fn.returnType === "any") {
violations.push({
ruleId: "QUAL-004",
severity: "error",
message: `函数 "${fn.name}" 的返回值类型为 any`,
location: { filePath: ctx.filePath, line: fn.line, column: 1 },
remediation: "为函数指定明确的返回类型。",
});
}
}
return violations;
},
};
5.3.6 内置规则集:安全约束
// === rules/security-rules.ts ===
import { ConstraintRule, AnalysisContext, Violation } from "../constraint-types";
/**
* SEC-001: 硬编码密钥检测
*/
export const noHardcodedSecretsRule: ConstraintRule = {
id: "SEC-001",
name: "禁止硬编码密钥",
description: "代码中不允许硬编码 API 密钥、密码、Token 等敏感信息",
severity: "error",
scope: "file",
check: (ctx: AnalysisContext): Violation[] => {
const violations: Violation[] = [];
// 匹配常见的密钥模式
const secretPatterns = [
{ pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"][A-Za-z0-9_\-]{16,}['"]/gi, name: "API Key" },
{ pattern: /(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]{4,}['"]/gi, name: "密码" },
{ pattern: /(?:secret|token|auth[_-]?token)\s*[:=]\s*['"][^'"]{8,}['"]/gi, name: "密钥/令牌" },
{ pattern: /(?:aws[_-]?access[_-]?key[_-]?id)\s*[:=]\s*['"]AKIA[A-Z0-9]{16}['"]/gi, name: "AWS Access Key" },
{ pattern: /(?:private[_-]?key)\s*[:=]\s*['"]-----BEGIN/gi, name: "私钥" },
{ pattern: /sk-[A-Za-z0-9]{20,}/g, name: "OpenAI Secret Key" },
];
const lines = ctx.content.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// 跳过注释行和测试文件中的 mock 数据
if (line.trim().startsWith("//") || line.trim().startsWith("*") || line.trim().startsWith("#")) continue;
if (ctx.filePath.includes("__tests__") || ctx.filePath.includes(".test.") || ctx.filePath.includes(".spec.")) continue;
for (const { pattern, name } of secretPatterns) {
if (pattern.test(line)) {
violations.push({
ruleId: "SEC-001",
severity: "error",
message: `检测到硬编码的${name}`,
location: { filePath: ctx.filePath, line: i + 1, column: 1 },
remediation: `将${name}移到环境变量中,使用 process.env.SECRET_NAME 或配置管理工具(如 Vault、AWS Secrets Manager)获取。`,
});
}
pattern.lastIndex = 0; // 重置正则状态
}
}
return violations;
},
};
/**
* SEC-002: SQL 注入防护
*/
export const sqlInjectionRule: ConstraintRule = {
id: "SEC-002",
name: "SQL 注入防护",
description: "禁止使用字符串拼接构建 SQL 查询,必须使用参数化查询",
severity: "error",
scope: "file",
check: (ctx: AnalysisContext): Violation[] => {
const violations: Violation[] = [];
// 匹配常见的 SQL 拼接模式
const sqlConcatPatterns = [
/(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER)\s+.*\$\{/gi, // 模板字符串拼接
/(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER)\s+.*['"]\s*\+/gi, // 字符串加法拼接
/(?:query|execute|raw)\s*\(\s*['"](?:SELECT|INSERT|UPDATE|DELETE)\s+.*\+/gi, // 函数调用中的拼接
];
const lines = ctx.content.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.trim().startsWith("//") || line.trim().startsWith("*")) continue;
for (const pattern of sqlConcatPatterns) {
if (pattern.test(line)) {
violations.push({
ruleId: "SEC-002",
severity: "error",
message: "检测到潜在的 SQL 注入风险:字符串拼接构建 SQL 查询",
location: { filePath: ctx.filePath, line: i + 1, column: 1 },
remediation: "使用参数化查询(Prepared Statement)或 ORM 的查询构建器。例如:db.query('SELECT * FROM users WHERE id = $1', [userId])",
});
}
pattern.lastIndex = 0;
}
}
return violations;
},
};
/**
* SEC-003: 不安全的反序列化检测
*/
export const unsafeDeserializationRule: ConstraintRule = {
id: "SEC-003",
name: "不安全反序列化",
description: "禁止对用户输入使用 eval()、Function() 或不安全的 JSON.parse",
severity: "error",
scope: "file",
check: (ctx: AnalysisContext): Violation[] => {
const violations: Violation[] = [];
const unsafePatterns = [
{ pattern: /\beval\s*\(/g, name: "eval()", suggestion: "使用 JSON.parse 或安全的表达式解析器" },
{ pattern: /\bnew\s+Function\s*\(/g, name: "new Function()", suggestion: "使用白名单方式处理动态逻辑" },
{ pattern: /\bvm\.runInNewContext\s*\(/g, name: "vm.runInNewContext()", suggestion: "使用隔离沙箱或 WASM 执行不可信代码" },
];
const lines = ctx.content.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.trim().startsWith("//") || line.trim().startsWith("*")) continue;
for (const { pattern, name, suggestion } of unsafePatterns) {
if (pattern.test(line)) {
violations.push({
ruleId: "SEC-003",
severity: "error",
message: `检测到不安全的 ${name} 调用`,
location: { filePath: ctx.filePath, line: i + 1, column: 1 },
remediation: suggestion,
});
}
pattern.lastIndex = 0;
}
}
return violations;
},
};
5.4.2 Python 内置规则集
# === python_rules.py ===
import ast
import re
from constraint_engine import (
AnalysisContext, ConstraintRule, Scope, Severity, Violation
)
class LayerDependencyRule(ConstraintRule):
"""
ARCH-P001: Python 项目分层架构约束
views -> services -> repositories -> models
"""
def __init__(self):
super().__init__(
rule_id="ARCH-P001",
name="分层架构依赖",
description="views 只能依赖 services,services 只能依赖 repositories。",
severity=Severity.ERROR,
scope=Scope.FILE,
)
self._layer_map = {
"views": ["services", "serializers"],
"services": ["repositories", "utils", "exceptions"],
"repositories": ["models", "utils", "exceptions"],
"models": [],
}
def check(self, context: AnalysisContext) -> list[Violation]:
violations: list[Violation] = []
current_layer = None
for layer in self._layer_map:
if f"/{layer}/" in context.file_path:
current_layer = layer
break
if not current_layer:
return violations
allowed_layers = self._layer_map.get(current_layer, [])
for imp in context.imports:
source = imp.get("module", "")
for layer in self._layer_map:
if f".{layer}." in source or source.endswith(f".{layer}"):
if layer not in allowed_layers and layer != current_layer:
violations.append(Violation(
rule_id=self.id,
severity=self.severity,
message=f"分层架构违规:{current_layer} 层不能导入 {layer} 层的 {source}",
file_path=context.file_path,
line=imp.get("line", 1),
column=1,
remediation=f'请通过依赖注入间接访问。允许依赖:{allowed_layers}',
))
return violations
class FunctionLengthRule(ConstraintRule):
"""
QUAL-P001: Python 函数长度限制
"""
def __init__(self, max_lines: int = 50):
super().__init__(
rule_id="QUAL-P001",
name="函数长度限制",
description=f"单个函数的代码行数不得超过 {max_lines} 行",
severity=Severity.WARNING,
scope=Scope.FILE,
)
self.max_lines = max_lines
def check(self, context: AnalysisContext) -> list[Violation]:
violations: list[Violation] = []
if context.ast_tree is None:
return violations
for node in ast.walk(context.ast_tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
end_line = getattr(node, "end_lineno", node.lineno + 1)
length = end_line - node.lineno + 1
if length > self.max_lines:
violations.append(Violation(
rule_id=self.id,
severity=self.severity,
message=f'函数 "{node.name}" 有 {length} 行,超过阈值 {self.max_lines}',
file_path=context.file_path,
line=node.lineno,
column=node.col_offset + 1,
remediation="将函数拆分为多个小函数。",
))
return violations
class NoHardcodedSecretsRule(ConstraintRule):
"""
SEC-P001: 硬编码密钥检测(Python 版)
"""
def __init__(self):
super().__init__(
rule_id="SEC-P001",
name="禁止硬编码密钥",
description="代码中不允许硬编码敏感信息",
severity=Severity.ERROR,
scope=Scope.FILE,
)
self._patterns = [
(re.compile(r'(?:api[_-]?key|apikey)\s*=\s*["\'][A-Za-z0-9_\-]{16,}["\']', re.I), "API Key"),
(re.compile(r'(?:password|passwd|pwd)\s*=\s*["\'][^"\']{4,}["\']', re.I), "密码"),
(re.compile(r'(?:secret|token|auth[_-]?token)\s*=\s*["\'][^"\']{8,}["\']', re.I), "密钥/令牌"),
)
def check(self, context: AnalysisContext) -> list[Violation]:
violations: list[Violation] = []
lines = context.content.split("\n")
for i, line in enumerate(lines, 1):
stripped = line.strip()
if stripped.startswith("#"):
continue
if "test" in context.file_path:
continue
for pattern, name in self._patterns:
if pattern.search(line):
violations.append(Violation(
rule_id=self.id,
severity=self.severity,
message=f"检测到硬编码的{name}",
file_path=context.file_path,
line=i,
column=1,
remediation=f'将{name}移到环境变量中,使用 os.environ 或 python-decouple。',
))
return violations
5.5 约束引擎与 Agent 的集成
5.5.1 Agent 工作流中的约束注入点
约束引擎的价值不在于它能检查代码,而在于它能在正确的时间点将检查结果反馈给 Agent。在 Harness Engineering 的框架中,有三个关键的约束注入点:
注入点1:预生成约束(Pre-generation Constraints)
在 Agent 开始生成代码之前,约束引擎将当前上下文相关的规则摘要注入到 Agent 的系统提示中。这不是把所有规则都塞进去,而是根据任务类型智能筛选:
// === constraint-injector.ts ===
import { ConstraintEngine, ConstraintRule } from "./constraint-engine";
/**
* 约束注入器 - 根据任务上下文筛选相关规则并生成提示摘要
*/
export class ConstraintInjector {
constructor(private engine: ConstraintEngine) {}
/**
* 根据任务类型和文件路径,生成约束提示摘要
*/
generatePromptHints(taskContext: {
type: "feature" | "bugfix" | "refactor" | "test";
targetFile?: string;
targetModule?: string;
}): string {
const rules = this.engine.getEnabledRules();
const relevantRules = this.filterRelevantRules(rules, taskContext);
const hints: string[] = [
"## 架构约束(自动验证,请确保代码符合以下规则)",
"",
];
for (const rule of relevantRules) {
hints.push(`- **[${rule.id}]** ${rule.name}:${rule.description}`);
}
hints.push("");
hints.push("> 以上规则将在代码提交前自动验证。违规将导致构建失败。");
return hints.join("\n");
}
/**
* 智能筛选与当前任务相关的规则
*/
private filterRelevantRules(
rules: ConstraintRule[],
ctx: { type: string; targetFile?: string; targetModule?: string }
): ConstraintRule[] {
return rules.filter(rule => {
// 安全规则始终包含
if (rule.id.startsWith("SEC")) return true;
// 根据目标文件路径筛选
if (ctx.targetFile) {
if (rule.id.startsWith("ARCH") && ctx.targetFile.includes("/controllers/")) {
return true;
}
}
// 质量规则对新功能和重构任务适用
if (rule.id.startsWith("QUAL") && ["feature", "refactor"].includes(ctx.type)) {
return true;
}
return false;
});
}
}
注入点2:后生成验证(Post-generation Validation)
这是最关键的注入点。Agent 生成代码后,约束引擎立即运行验证,并将结果作为反馈消息返回给 Agent:
// === agent-harness-middleware.ts ===
import { ConstraintEngine } from "./constraint-engine";
import { TypeScriptAnalyzer } from "./code-analyzer";
/**
* Agent Harness 中间件
* 在 Agent 生成代码后自动运行约束验证
*/
export class AgentHarnessMiddleware {
private analyzer = new TypeScriptAnalyzer();
constructor(private engine: ConstraintEngine) {}
/**
* 处理 Agent 生成的代码
*/
async processGeneratedCode(filePath: string, content: string): Promise<{
passed: boolean;
feedback: string;
violationCount: number;
}> {
const context = this.analyzer.analyze(filePath, content);
const report = await this.engine.validate(context);
const feedback = this.engine.formatFeedback(report);
return {
passed: report.passed,
feedback,
violationCount: report.totalViolations,
};
}
/**
* 自动修复循环:验证 -> 反馈 -> 修复 -> 再验证
* 最多迭代 3 次
*/
async validateAndFix(
filePath: string,
content: string,
fixFunction: (feedback: string, currentCode: string) => Promise<string>
): Promise<{ finalContent: string; iterations: number; passed: boolean }> {
let currentContent = content;
const MAX_ITERATIONS = 3;
for (let i = 0; i < MAX_ITERATIONS; i++) {
const result = await this.processGeneratedCode(filePath, currentContent);
if (result.passed) {
return { finalContent: currentContent, iterations: i + 1, passed: true };
}
// 将验证反馈传递给 Agent,让它修复
currentContent = await fixFunction(result.feedback, currentContent);
}
const finalResult = await this.processGeneratedCode(filePath, currentContent);
return {
finalContent: currentContent,
iterations: MAX_ITERATIONS,
passed: finalResult.passed,
};
}
}
注入点3:提交门禁(Commit Gate)
作为最后一道防线,通过 Git pre-commit hook 确保任何代码(无论来源是人工还是 AI)都不会绕过约束:
#!/bin/bash
# .husky/pre-commit
echo "Running Harness constraint check..."
STAGED_TS_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.ts$')
if [ -z "$STAGED_TS_FILES" ]; then
echo "No TypeScript files to check"
exit 0
fi
npx ts-node scripts/run-constraints.ts $STAGED_TS_FILES
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
echo ""
echo "Constraint check failed. Please fix the issues above."
exit 1
fi
echo "All constraint checks passed"
5.5.2 渐进式约束策略
在实际项目中,不建议一开始就启用所有约束。更好的策略是渐进式严格:
| 阶段 | 时间 | 策略 | 规则集 | 严格程度 |
|---|---|---|---|---|
| 观察期 | 第 1-2 周 | 只记录,不阻断 | 全部规则设为 info | 仅统计违规数量 |
| 警告期 | 第 3-4 周 | 警告但不阻断 | 关键规则设为 warning | 建立基线指标 |
| 执行期 | 第 5 周起 | 严格阻断 | 关键规则设为 error | CI/CD 门禁 |
| 优化期 | 持续 | 根据数据调整 | 动态调整阈值 | 持续改进 |
关键实践:基线冻结
在从观察期过渡到执行期时,一个有效的策略是基线冻结:
- 记录当前所有违规(作为基线)
- 对基线中的历史违规发放豁免令牌(允许在限定时间内修复)
- 新增代码必须完全合规
- 豁免令牌到期后,历史违规也变为阻断性错误
// === baseline-manager.ts ===
import * as fs from "fs";
import { Violation } from "./constraint-types";
interface BaselineEntry {
ruleId: string;
filePath: string;
line: number;
message: string;
exemptUntil: string; // ISO date
}
export class BaselineManager {
private baseline: Map<string, BaselineEntry> = new Map();
/**
* 从当前违规列表创建基线
*/
freezeBaseline(violations: Violation[], exemptionDays: number = 30): void {
const exemptUntil = new Date(Date.now() + exemptionDays * 86400000).toISOString();
for (const v of violations) {
const key = `${v.ruleId}:${v.location.filePath}:${v.location.line}`;
this.baseline.set(key, {
ruleId: v.ruleId,
filePath: v.location.filePath,
line: v.location.line,
message: v.message,
exemptUntil,
});
}
fs.writeFileSync(
".harness-baseline.json",
JSON.stringify(Array.from(this.baseline.entries()), null, 2)
);
}
/**
* 过滤掉基线中的豁免违规,只返回新增违规
*/
filterNewViolations(violations: Violation[]): Violation[] {
const now = new Date().toISOString();
return violations.filter(v => {
const key = `${v.ruleId}:${v.location.filePath}:${v.location.line}`;
const entry = this.baseline.get(key);
if (!entry) return true;
return entry.exemptUntil < now;
});
}
loadBaseline(): void {
if (!fs.existsSync(".harness-baseline.json")) return;
const data = JSON.parse(fs.readFileSync(".harness-baseline.json", "utf-8"));
this.baseline = new Map(data);
}
}
5.5.3 约束指标与可观测性
约束引擎不仅产生验证报告,还应该提供可观测性指标,帮助团队了解 Agent 的架构合规趋势:
// === constraint-metrics.ts ===
export interface ConstraintMetrics {
/** 各规则的通过率 */
rulePassRates: Map<string, number>;
/** 违规趋势(按天统计) */
violationTrend: { date: string; count: number }[];
/** 最常违反的规则 Top 5 */
topViolations: { ruleId: string; count: number; description: string }[];
/** Agent 的修复成功率 */
autoFixSuccessRate: number;
/** 平均违规数/提交 */
avgViolationsPerCommit: number;
}
export class MetricsCollector {
private dailyViolations: Map<string, number> = new Map();
private ruleViolationCounts: Map<string, number> = new Map();
private totalCommits = 0;
private totalViolations = 0;
private fixAttempts = 0;
private fixSuccesses = 0;
recordValidation(violations: Violation[]): void {
const today = new Date().toISOString().slice(0, 10);
this.dailyViolations.set(today, (this.dailyViolations.get(today) || 0) + violations.length);
this.totalCommits++;
this.totalViolations += violations.length;
for (const v of violations) {
this.ruleViolationCounts.set(v.ruleId, (this.ruleViolationCounts.get(v.ruleId) || 0) + 1);
}
}
recordFixAttempt(success: boolean): void {
this.fixAttempts++;
if (success) this.fixSuccesses++;
}
getMetrics(): ConstraintMetrics {
const trend = Array.from(this.dailyViolations.entries())
.map(([date, count]) => ({ date, count }))
.sort((a, b) => a.date.localeCompare(b.date));
const topViolations = Array.from(this.ruleViolationCounts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([ruleId, count]) => ({ ruleId, count, description: "" }));
return {
rulePassRates: new Map(),
violationTrend: trend,
topViolations,
autoFixSuccessRate: this.fixAttempts > 0 ? this.fixSuccesses / this.fixAttempts : 0,
avgViolationsPerCommit: this.totalCommits > 0 ? this.totalViolations / this.totalCommits : 0,
};
}
}
5.6 企业级案例:某金融科技公司的约束驱动开发
5.6.1 背景
2025年下半年,一家金融科技公司(我们称其为 FinSecure)面临一个典型困境。公司在年初全面引入了 AI Agent 辅助开发,团队规模从 20 人扩展到 35 人(其中 15 人是新入职的初级工程师),AI Agent 的代码生成占比达到了 60%。
问题浮现
在引入 AI Agent 的前三个月,一切看起来很美好:
| 指标 | 引入前 | 引入后(3个月) | 变化 |
|---|---|---|---|
| 功能交付速度 | 8 个/月 | 14 个/月 | +75% |
| Bug 数量 | 12 个/月 | 11 个/月 | -8% |
| 代码审查通过率 | 85% | 87% | +2% |
但六个月后,情况急转直下:
| 指标 | 引入后(3个月) | 引入后(6个月) | 变化 |
|---|---|---|---|
| 功能交付速度 | 14 个/月 | 9 个/月 | -36% |
| Bug 数量 | 11 个/月 | 34 个/月 | +209% |
| 代码审查通过率 | 87% | 52% | -40% |
| 安全漏洞 | 0 个 | 7 个 | 新增 |
| 架构违规 | ~5 处/月 | ~45 处/月 | +800% |
根本原因:AI Agent 生成的代码在表面上看起来正确(编译通过、测试通过),但大量违反了公司的架构约定和安全规范。 这些"隐性违规"在代码审查阶段难以发现,在运行时才暴露出来。
5.6.2 约束系统的设计与实施
FinSecure 的工程 VP 决定引入机械化约束系统。整个实施分为四个阶段:
第一阶段:规则梳理(1周)
团队花了整整一周时间,从架构文档、代码审查记录、Bug 报告和 Post-Mortem 分析中,梳理出了公司最关心的架构规则:
第二阶段:引擎开发与集成(2周)
团队基于本章介绍的约束引擎架构,开发了一个定制化的约束系统。关键定制点:
- 金融领域特定规则
// 金融领域特有的约束:所有金额计算必须使用 Decimal
const decimalArithmeticRule: ConstraintRule = {
id: "FIN-001",
name: "金额计算必须使用 Decimal",
description: "所有涉及金额的运算必须使用 decimal.js 或 big.js,禁止使用 JavaScript 原生 number",
severity: "error",
scope: "file",
check: (ctx: AnalysisContext): Violation[] => {
const violations: Violation[] = [];
const lines = ctx.content.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// 检测可能的金额浮点运算
if (/(?:price|amount|total|balance|fee|cost|revenue)\s*[\+\-\*\/]=?\s*\d/.test(line)) {
if (!line.includes("Decimal") && !line.includes("Big") && !line.includes("decimal")) {
violations.push({
ruleId: "FIN-001",
severity: "error",
message: "金额计算疑似使用了 JavaScript 原生 number 类型",
location: { filePath: ctx.filePath, line: i + 1, column: 1 },
remediation: "使用 new Decimal(amount).plus(other) 代替 amount + other。浮点精度问题在金融场景中可能导致资金错误。",
});
}
}
}
return violations;
},
};
// 金融领域:所有数据库操作必须有事务
const transactionRequiredRule: ConstraintRule = {
id: "FIN-002",
name: "写操作必须在事务中执行",
description: "所有涉及数据写入的 Service 方法必须在事务上下文中执行",
severity: "error",
scope: "file",
check: (ctx: AnalysisContext): Violation[] => {
const violations: Violation[] = [];
for (const cls of ctx.classes) {
if (!ctx.filePath.includes("/services/")) continue;
for (const method of cls.methods) {
// 检查方法是否包含写操作
const hasWriteOp = /(?:create|update|delete|insert|save|remove)\w*\(/.test(method.body);
const hasTransaction = /(?:transaction|trx|withTransaction|@Transactional)/.test(method.body);
if (hasWriteOp && !hasTransaction) {
violations.push({
ruleId: "FIN-002",
severity: "error",
message: `Service 方法 "${cls.name}.${method.name}" 包含写操作但缺少事务包裹`,
location: { filePath: ctx.filePath, line: method.line, column: 1 },
remediation: "将写操作包裹在事务中:await this.db.transaction(async (trx) => { ... })",
});
}
}
}
return violations;
},
};
- 合规审计日志规则
// 金融合规:敏感操作必须记录审计日志
const auditLogRule: ConstraintRule = {
id: "COMP-001",
name: "敏感操作审计日志",
description: "涉及用户资金、权限变更、数据导出等操作必须记录审计日志",
severity: "error",
scope: "file",
check: (ctx: AnalysisContext): Violation[] => {
const violations: Violation[] = [];
const sensitivePatterns = [
/(?:transfer|withdraw|deposit|refund|payment)/i,
/(?:grantRole|revokeRole|changePermission|updateRole)/i,
/(?:exportData|bulkDownload|dataDump)/i,
];
for (const fn of ctx.functions) {
const isSensitiveOp = sensitivePatterns.some(p => p.test(fn.name));
const hasAuditLog = /auditLog|audit\.log|logAudit|recordAudit/.test(fn.body);
if (isSensitiveOp && !hasAuditLog) {
violations.push({
ruleId: "COMP-001",
severity: "error",
message: `函数 "${fn.name}" 疑似敏感操作但缺少审计日志记录`,
location: { filePath: ctx.filePath, line: fn.line, column: 1 },
remediation: "在操作完成后调用 auditLog.record({ action, userId, resource, details }) 记录审计日志。",
});
}
}
return violations;
},
};
第三阶段:渐进式推广(4周)
按照渐进式策略执行:
- 第 1-2 周(观察期):全部规则设为 info 级别,仅收集数据。结果触目惊心——在 AI 生成的代码中,平均每次提交有 8.3 处违规。
- 第 3-4 周(警告期):安全规则和架构规则设为 warning。团队开始关注约束反馈。
- 第 5 周起(执行期):安全规则设为 error,CI 门禁启用。
第四阶段:效果评估(持续)
实施三个月后的数据对比:
| 指标 | 实施前 | 实施后(1个月) | 实施后(3个月) | 变化 |
|---|---|---|---|---|
| 每次提交平均违规数 | 8.3 | 4.1 | 0.8 | -90% |
| 安全漏洞 | 7 个/季 | 2 个/季 | 0 个/季 | -100% |
| 架构违规 | ~45 处/月 | ~18 处/月 | ~3 处/月 | -93% |
| Agent 代码首次通过率 | 52% | 71% | 91% | +75% |
| 代码审查耗时 | 3.2 小时/PR | 2.1 小时/PR | 1.4 小时/PR | -56% |
| 生产 Bug | 34 个/月 | 18 个/月 | 9 个/月 | -74% |
5.6.3 关键经验教训
经验一:约束不是越多越好
FinSecure 最初设计了 45 条规则,但发现其中 12 条产生了大量误报(false positive)。过多的误报会导致"告警疲劳"——开发者开始忽略所有告警,包括真正重要的。
最佳实践:保持规则数量在 15-25 条之间,确保每条规则的误报率低于 5%。
经验二:修复建议比错误检测更重要
仅仅告诉 Agent “你违反了规则 X” 是不够的。Agent 需要知道如何修复。FinSecure 发现,包含详细修复建议的规则,Agent 的首次修复成功率从 45% 提升到 82%。
经验三:约束规则需要版本化
约束规则本身也需要版本控制。当团队决定修改某条规则的阈值时(比如将函数长度上限从 50 行改为 60 行),应该像代码一样提交 PR、经过审查、记录变更原因。
// 约束规则版本化配置
interface RuleVersion {
version: string; // 语义版本号
effectiveDate: string; // 生效日期
changelog: string; // 变更说明
author: string; // 修改人
approvedBy: string; // 审批人
}
5.7 最佳实践与反模式
5.7.1 最佳实践
实践一:约束即文档
每条约束规则都应该是架构决策的活文档。当新成员加入团队时,阅读约束规则集就能快速理解项目的架构约定。
实践二:约束规则的分类管理
rules/
architecture/ # 架构约束
layer-dependency.ts
circular-deps.ts
module-boundary.ts
security/ # 安全约束
no-hardcoded-secrets.ts
sql-injection.ts
input-validation.ts
quality/ # 质量约束
complexity.ts
function-length.ts
documentation.ts
domain/ # 领域特定约束
financial-calc.ts
audit-log.ts
data-classification.ts
实践三:约束规则测试
约束规则本身也需要测试。每条规则应该有:
// === rules/__tests__/layer-dependency.test.ts ===
import { describe, it, expect } from "vitest";
import { layerDependencyRule } from "../architecture/layer-dependency";
describe("ARCH-001: 分层架构依赖", () => {
it("应该允许 Controller 导入 Service", () => {
const ctx = createMockContext({
filePath: "src/controllers/orderController.ts",
imports: [{ source: "../services/orderService", specifiers: ["OrderService"], isTypeOnly: false, line: 1 }],
});
const violations = layerDependencyRule.check(ctx);
expect(violations).toHaveLength(0);
});
it("应该禁止 Controller 直接导入 Repository", () => {
const ctx = createMockContext({
filePath: "src/controllers/orderController.ts",
imports: [{ source: "../repositories/orderRepository", specifiers: ["OrderRepository"], isTypeOnly: false, line: 1 }],
});
const violations = layerDependencyRule.check(ctx);
expect(violations).toHaveLength(1);
expect(violations[0].ruleId).toBe("ARCH-001");
});
it("应该忽略非业务层的导入(如第三方库)", () => {
const ctx = createMockContext({
filePath: "src/controllers/orderController.ts",
imports: [{ source: "express", specifiers: ["Router"], isTypeOnly: false, line: 1 }],
});
const violations = layerDependencyRule.check(ctx);
expect(violations).toHaveLength(0);
});
it("应该允许 Service 导入 Repository", () => {
const ctx = createMockContext({
filePath: "src/services/orderService.ts",
imports: [{ source: "../repositories/orderRepository", specifiers: ["OrderRepository"], isTypeOnly: false, line: 1 }],
});
const violations = layerDependencyRule.check(ctx);
expect(violations).toHaveLength(0);
});
});
实践四:与 Agent 的反馈闭环
约束引擎的反馈应该被设计成 Agent 能直接理解和行动的格式:
## 约束验证报告
### 结果:❌ 未通过(2 error / 1 warning)
### 必须修复(error)
1. **[SEC-001]** 第 15 行:检测到硬编码的 API Key
修复方式:将 `const apiKey = "sk-abc..."` 替换为 `const apiKey = process.env.API_KEY`
2. **[ARCH-001]** 第 3 行:Controller 不能直接导入 Repository
修复方式:删除 `import { OrderRepository }` ,改为注入 `OrderService`
### 建议修复(warning)
3. **[QUAL-003]** 第 22 行:导出的函数缺少 JSDoc 文档
修复方式:在函数声明前添加 /** @description ... @param ... @returns ... */
5.7.2 常见反模式
反模式一:约束过细
// ❌ 反模式:过度约束
const variableNamingRule: ConstraintRule = {
id: "STYLE-001",
name: "变量命名规则",
description: "所有循环变量必须命名为 i, j, k", // 太死板了!
// ...
};
好的约束划定边界,但不扼杀创造力。变量命名应该由 ESLint 的 naming-convention 规则处理(足够灵活),而非硬编码在约束引擎中。
反模式二:约束不一致
规则 A:所有 Service 方法必须有错误处理
规则 B:工具函数不需要错误处理(让调用方处理)
规则 C:Service 中调用的工具函数...(没说清楚)
当规则之间存在灰色地带时,Agent 会在不同规则之间"反复横跳",每次生成可能遵循不同的规则。
反模式三:约束无反馈
// ❌ 反模式:只检查,不告诉怎么修
check: (ctx) => {
if (/* 违规 */) {
return [{ message: "不符合架构规范" }]; // 然后呢??
}
}
反模式四:忽视误报
当一条规则的误报率超过 10% 时,它会迅速失去团队的信任。正确做法是:
- 立即将规则降级为 warning 或 info
- 分析误报模式
- 改进规则的检测逻辑
- 在测试中覆盖误报场景
- 重新升级为 error
反模式五:约束绕过机制缺失
有些约束在特定场景下确实需要被绕过(比如紧急修复、原型验证)。如果没有正式的绕过机制,开发者会采取更隐蔽的方式(如 // eslint-disable)。
最佳实践:提供正式的绕过流程:
// @harness-exempt ARCH-001 紧急修复 #12345,有效期至 2025-12-31
import { OrderRepository } from "../repositories/orderRepository";
绕过记录会被自动追踪,超过有效期后自动恢复为阻断性错误。
5.8 本章小结
5.8.1 核心要点回顾
本章探讨了机械化架构约束的完整理论与实践。让我们回顾核心要点:
| 要点 | 说明 |
|---|---|
| 约束优于指令 | 告诉 Agent "什么不能做"比"怎么做"更有效,因为约束是可验证的、确定的、不可绕过的 |
| 四层防线 | 编译时约束 → 静态分析约束 → 架构约束 → 运行时约束,形成纵深防御 |
| 形式化表达 | 每条约束必须包含 ID、作用域、判定条件、严重级别和修复建议五个要素 |
| 三个注入点 | 预生成约束引导 → 后生成验证反馈 → 提交门禁阻断 |
| 渐进式严格 | 观察期 → 警告期 → 执行期 → 优化期,避免一步到位引起抵触 |
| 反馈闭环 | 约束引擎的检测结果必须以 Agent 可理解的格式反馈,形成自动修复循环 |
| 可观测性 | 约束指标(通过率、违规趋势、修复成功率)是持续改进的基础 |
5.8.2 约束系统与其他 Harness 组件的关系
协作流程:
- 知识系统 → 约束引擎:知识系统提供项目的架构文档、编码规范和历史决策记录,约束引擎将这些知识编码为可执行规则
- 约束引擎 → 可观测性:约束引擎的验证结果(违规数量、类型、趋势)被发送到可观测性系统,形成架构健康度指标
- 可观测性 → 自修复闭环:当可观测性系统检测到架构合规度下降时,触发自修复闭环,自动修复最常见的违规模式
- 约束引擎 → Agent 互审:约束引擎的验证报告作为 Agent 互审的客观依据,减少审查中的主观争议
5.8.3 思考题
基础题
- 列举你所在项目中 AI Agent 最常犯的 5 个架构违规,并为每个违规设计一条形式化的约束规则。
- 解释"渐进式严格"策略的四个阶段,并说明为什么不建议从一开始就启用所有约束。
- 比较"预生成约束"和"后生成验证"两种注入方式的优缺点。
进阶题
- 设计一套约束规则的测试框架,要求能覆盖正常通过、违规检测、误报排除和边界条件四种场景。
- 在你的项目中实现一个"基线冻结"系统,要求支持:创建基线、过期管理、增量违规检测。
- 分析约束规则的"完备性"和"一致性"之间的矛盾,给出你在实践中如何平衡这两者的策略。
开放题
- 如果约束规则本身也由 AI Agent 来生成和维护,会带来哪些新的挑战和机遇?设计一个"自适应约束系统"的架构草案。
- 讨论机械化约束的局限性——有哪些重要的架构决策是难以用机器验证的?对于这些决策,你有什么替代方案?
下一章预告:在第 6 章中,我们将探讨可观测性注入——如何让你的 AI Agent 系统从"黑箱"变为"白箱",通过分布式追踪、实时指标和智能告警,实现对 Agent 行为的全方位洞察。
更多推荐


所有评论(0)