文章指出,测试通过和 PR 检查正常仍可能掩盖代码偏离需求的问题。作者提出结合 StrictDoc、Tracey 与 Git 构建软件意图图,以可追踪的需求关系约束编码 Agent。
简要概述:AI 编码智能体编写代码的速度是人类验证速度的 10 倍。测试通过、PR 全绿,往往掩盖了悄然发生的规格漂移。本文将介绍如何使用 StrictDoc 和 Tracey 构建确定性的、Git 原生的软件意图图谱,通过机械化检查来治理编码智能体。
软件工程始终面临两股相互竞争的力量之间的持续拉扯:一方面希望快速交付功能,另一方面又必须确保交付的功能确实符合最初的意图。在这个领域的大部分历史中,这两股力量一直处于一种可控的平衡状态。人类开发者受打字速度、认知负荷以及上下文切换需求的限制,产出代码的速度,自然地与其他人审查、测试和验证这些代码的速度大致相当。这种平衡并不完美——缺陷会随产品发布,需求会发生漂移,文档会过时——但它是稳定的。系统能够容忍这种漂移,因为漂移积累得很慢。
自主 AI 编码智能体从根本上打破了这种平衡。当一个智能体能够在人类审查者读完规格说明中一个段落的时间内,生成上千行看似合理、语法有效、甚至正确得令人意外的代码时,生成速度与验证速度之间的比例就会严重失衡。这就是 AI 速度悖论:承诺加速交付的工具,也在加速系统实际状态与系统预期状态之间的脱节。更关键的是,这种脱节在发生时几乎总是不可见的。代码能够编译。测试全部通过。拉取请求一片绿色。没有任何警报。然而,悄无声息之间,支撑这些代码存在的意图已经被抛在身后。
本文展示的概念和代码,直接取自电子书《使用 StrictDoc 与 Tracey 技术栈治理 AI 编码智能体:通过动态意图图谱控制规格漂移》中提出的完整路线图。另可参阅共 9 卷的《TypeScript AI 与智能体工程师大师课》,其中也包含一卷关于 Jev 的内容。
我们先从一个基本的理论判断开始:规格漂移不是缺陷,而是热力学意义上的默认结果。如果没有一种主动执行、强制生效的机制将意图与实现绑定起来,两者就会分道扬镳。无一例外。只要时间足够长、参与者足够多、变更压力足够大,每个系统都会逐渐偏离最初赋予它存在理由的意图。这并不是开发者在职业操守上的失误;对于任何知识系统,只要同一个想法的不同表示形式由不同工具维护在不同位置,并面向不同受众,这就是它固有的结构性属性。
我们可以将其视为软件热力学第二定律:除非施加外部功来逆转这一趋势,否则意图与实现之间的一致性熵总会趋于增加。这些外部功——审查、测试、可追溯性核查、验证关卡——是有限的。而崩溃恰恰就发生在这种有限性上。
这里一个经典的类比是物理存储介质中的位腐化。磁盘上的磁性粒子会逐渐失去原有排列;CD 会出现微小划痕;磁带会老化。数据不会一下子全部消失,而是逐渐损坏——这里翻转一个比特,那里读错一个扇区。依赖这些数据的系统,要么通过主动校验和验证发现问题,要么在毫无察觉的情况下发生数据损坏。静默规格漂移相当于语义层面的位腐化,只不过它作用于我们称为“系统意图”的更高层表示。位腐化破坏字节,规格漂移则破坏含义。而且,除非我们主动构建检测机制,否则它发生时不会有任何校验和机制来发现它。
每个工程组织的验证带宽都是有限的:也就是它能够真正验证代码是否完成预期工作的速率。在传统软件开发中,这些带宽主要消耗在人类代码审查上。审查者阅读 diff,将相关需求保存在工作记忆中,对照这些需求评估变更,然后批准或拒绝。这种方式勉强能够奏效,是因为 diff 足够小,审查者的工作记忆可以同时容纳变更及其意图。
AI 智能体通过两种不同的方式破坏了这一过程。
首先,它们放大了变更量。一次提示就可能生成一个涉及二十个文件的拉取请求。审查者的工作记忆无法容纳二十个文件的上下文,于是开始略读。略读是验证的终结。略读式审查只是名义上的审查——它是一种仪式,提供了验证的感觉,却没有验证的实质。
其次,更隐蔽的是,它们提高了变更的表面合理性密度。AI 生成的代码单独看往往显得正确。它采用惯用模式、合理的变量名和恰当的库调用。审查者识别错误代码的“蜘蛛感应”,是针对人类编写代码时常犯的错误训练出来的。在阅读 AI 输出时,这种直觉较少被触发,因为 AI 输出在表面上更符合训练分布中那些“看起来正确的代码”。这意味着,合理性在过去只是正确性的一个弱信号,到了 AI 时代,却成了一个彻底误导人的信号。
为了让这种意图图谱真正运作起来,同时不依赖笨重的企业工具或封闭的商业平台,我们将两个轻量级开源引擎结合使用:
**StrictDoc(Python):**一个 Git 原生的“需求即代码”平台。它用结构化规格说明(.sdoc)替代被动的 Markdown 文件和 Jira 工单,强制执行验证规则、正式的 EARS 语法以及持久化的机器标识符(MID)。它定义系统必须做什么。
**Tracey(Rust):**一个高性能的规格覆盖与验证引擎。它使用 Tree-sitter 解析 30 多种语言中的代码注释,通过机械化检查,验证需求是否已经实现(// r[impl]),以及是否有测试加以证明(// r[verify])。它跟踪代码实际做了什么,在规格发生变更时标记过时的引用,并通过 Model Context Protocol(MCP)向 AI 智能体暴露意图图谱。
两者共同形成一个相互依存、没有任何厂商锁定的闭环:StrictDoc 为意图建模;Tracey 验证现实。
传统规格说明存在于文档中。文档是一种线性的、以散文组织、为人类阅读优化的产物。它有开头、中间和结尾。人们按顺序阅读它。它用自然语言编写,表达力丰富,却存在歧义。而且,关键在于,它没有内部状态。文档不知道自己是否得到了遵循,或者是否遭到了违反。它无法回答问题,也无法报告自身的一致性。它只是被动的数据。
StrictDoc 和 Tracey 结合起来所实现的理论转变,是从文档转向图谱。图谱由节点和边构成:节点表示独立、可识别的需求,边表示关系——父子、同级、细化、验证、实现。图谱具有可查询的结构,也具有可分析的拓扑。我们可以遍历它、检查环路、查找孤立节点,最重要的是,还可以将其关联到源代码文件、测试函数和验证注解等外部实体。
一旦意图以图谱而非文档的形式存在,双向引用完整性就成为可能。每个代码产物都可以携带指向其所实现需求的引用。每个测试产物都可以携带指向其所验证需求的引用。反过来,对于每个需求,我们也可以查询其上游依据和下游实现。图谱由此成为连接意图、代码和测试的纽带,将三者绑定为一个连贯的整体。
图谱是治理规格漂移的必要条件,但并不充分。一个只构建一次、此后从不更新的图谱,与一份只写一次、此后从不更新的文档一样陈旧。治理系统与绘图工具的区别在于,图谱必须是动态的:它必须持续从权威来源重建,持续对照这些来源进行验证,并持续接受正在实施变更的人类和机器的查询。
让意图图谱保持“动态”的特性有三个:
**持续性。**不允许图谱与其源文档失去同步。只要磁盘上的规格文件发生变更,图谱就会以增量方式重建。
**可查询性。**图谱以一组查询接口的形式对外提供,随时可以发起查询。“哪些需求尚无实现?”“哪些实现引用了过时的版本?”
强制执行。这个图谱不仅提供信息,还具有约束力。失效引用、过期引用和孤立需求都会被判定为流水线失败。
设想一个中型 SaaS 团队正在交付一套多租户计费服务。他们的编码智能体能够在不到 90 秒的时间里,生成可运行的 stripe-webhook 处理程序、JWT 刷新端点及其测试套件。这种速度正是团队采用智能体的全部原因,也恰恰是他们开始在不知不觉中交付回归缺陷的原因。
下面,我们将 StrictDoc 和 Tracey 接入一个真实的 TypeScript 项目,借此强制落实一个持续维护的意图图谱。
我们从规格说明入手。StrictDoc 的 .sdoc 语法会将需求解析为具有稳定 UID 的一等对象。
[DOCUMENT]
TITLE: Northwind Authentication Intent
OPTIONS:
AUTO_UIDS: True
MARKUP: Markdown
[[SECTION]]
TITLE: Session Lifecycle
[REQUIREMENT]
UID: auth.login.token-ttl
STATUS: Approved
TITLE: Login token time-to-live
STATEMENT: >>>
The login endpoint shall issue a JSON Web Token whose `exp` claim is set to
exactly 86,400 seconds (24 hours) after the token's `iat` claim.
Any token whose `exp - iat` differs from 86,400 seconds shall be rejected
during verification, regardless of signature validity.
<<<
RATIONALE: >>>
A fixed, auditable TTL is required by SOC 2 control CC6.1 ("logical
access security"). Agents generating code must not drift toward shorter
"safer-looking" TTLs without an explicit, versioned spec change.
<<<
RELATIONS:
- TYPE: Parent
VALUE: auth.session.root
[/SECTION]
Tracey 使用声明式配置文件(.config/tracey/config.styx),将文档文件与源代码和测试目录关联起来。
@schema {id crate:tracey-config@1, cli tracey}
specs (
{
name northwind-auth
source_url https://github.com/northwind/api/blob/main/docs/spec/auth.sdoc
include (docs/spec/**/*.sdoc)
impls (
{
name typescript-backend
include (src/**/*.ts)
exclude (src/**/*.generated.ts dist/** node_modules/**)
test_include (tests/**/*.test.ts)
}
)
}
)
在 TypeScript 后端源代码中,我们使用 JSDoc 风格的 // r[impl ...] 注解,将代码块直接绑定到上游 StrictDoc 需求的 UID。
// src/auth/login.ts
import jwt, { JwtPayload, SignOptions } from "jsonwebtoken";
import { z } from "zod";
export const TOKEN_TTL_SECONDS = 86_400;
export const LoginRequestSchema = z.object({
email: z.string().email(),
password: z.string().min(1),
});
export type LoginRequest = z.infer<typeof LoginRequestSchema>;
export interface LoginResult {
token: string;
expiresAt: number;
}
/**
* Issues a signed JWT for a user, with an `exp` exactly
* `TOKEN_TTL_SECONDS` after the current time.
*
* // r[impl auth.login.token-ttl]
*/
export async function login(request: LoginRequest): Promise<LoginResult> {
const parsed = LoginRequestSchema.parse(request);
const user = await verifyCredentials(parsed.email, parsed.password);
if (!user) {
throw new Error("INVALID_CREDENTIALS");
}
const issuedAt = Math.floor(Date.now() / 1000);
const signOptions: SignOptions = {
expiresIn: TOKEN_TTL_SECONDS,
algorithm: "RS256",
};
const token = jwt.sign(
{ sub: user.id, email: user.email },
getPrivateKey(),
signOptions,
);
const decoded = jwt.decode(token) as JwtPayload;
if (!decoded.exp || !decoded.iat) {
throw new Error("TOKEN_MISSING_TIMECLAIMS");
}
if (decoded.exp - decoded.iat !== TOKEN_TTL_SECONDS) {
throw new Error("TOKEN_TTL_DRIFT");
}
return { token, expiresAt: decoded.exp };
}
async function verifyCredentials(
email: string,
password: string,
): Promise<{ id: string; email: string } | null> {
return { id: "u_123", email };
}
function getPrivateKey(): string {
return process.env.JWT_PRIVATE_KEY ?? "";
}
测试同样通过 // r[verify ...] 绑定到完全相同的 UID,从而建立完整的验证闭环。
// tests/auth/login.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import {
login,
TOKEN_TTL_SECONDS,
LoginRequestSchema,
} from "../../src/auth/login";
import jwt from "jsonwebtoken";
describe("POST /auth/login — token lifetime", () => {
beforeEach(() => {
vi.stubEnv("JWT_PRIVATE_KEY", process.env.__TEST_KEY ?? "test-key");
});
/**
* The canonical happy-path test for the TTL requirement.
*
* // r[verify auth.login.token-ttl]
*/
it("issues a token whose exp - iat is exactly 86,400 seconds", async () => {
const { token } = await login({
email: "alice@northwind.example",
password: "correct-horse-battery-staple",
});
const decoded = jwt.decode(token) as { exp: number; iat: number };
expect(decoded.exp - decoded.iat).toBe(TOKEN_TTL_SECONDS);
});
});
为了防止未经验证的变更进入生产环境,我们在 CI 流水线中强制执行意图图谱约束。如果 AI 智能体试图交付缺少有效、最新映射的代码,构建就会立即失败。
name: Specification Assurance Gate
on: [push, pull_request]
jobs:
verify-intent:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Tracey
run: |
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/bearcove/tracey/releases/latest/download/tracey-installer.sh | sh
echo "{% katex inline %}HOME/.cargo/bin" >> {% endkatex %}GITHUB_PATH
- name: Install StrictDoc
run: pip install strictdoc
- name: Validate StrictDoc Document Tree
run: strictdoc export --formats=html .
- name: Enforce Zero-Warning Spec Coverage & Traceability
run: tracey query validate --deny warnings
- name: Refuse untested requirements
run: |
if tracey query untested | grep -qv '^$'; then
echo "::error::Unverified requirements detected."
tracey query untested
exit 1
fi
团队首次将自动化可追溯性工具与自主编码智能体配合使用时,往往会遇到几个可以预见的障碍:
臆造需求 ID: 在没有明确提示的情况下,智能体可能会编造看似合理的 UID,例如使用 auth.login.token-expiry,而不是 auth.login.token-ttl。StrictDoc 和 Tracey 会在验证时将其识别为失效引用,但前提是 CI 流水线确实会因警告而阻断。
在测试文件中使用 impl 注解: 将实现注解放进测试文件,会违反结构分离原则。测试必须仅使用 verify 通道。
只提升版本,不修复代码: 需求发生变化时,智能体有时只会提升注解的版本后缀,以消除 CI 警告,却不更新实际的业务逻辑。必须严格执行这条规则:先更新代码,再验证测试,最后才能提升注解版本。
自主 AI 编码智能体让工程生产力实现了质的飞跃,但也极大加剧了软件设计意图与实际执行行为之间的无声漂移。面对机器级速度,传统的人工审查、缺乏关联的文档和孤立的测试,已不足以维持系统的完整性。
通过将被动文档替换为持续维护的意图图谱——以 StrictDoc 为锚点,通过 Tracey 建立映射,经由 Model Context Protocol(MCP,模型上下文协议)向 AI 智能体开放,并在 CI 中严格执行约束——我们重新掌握了对代码库的控制权。规格漂移不再是不可避免的热力学衰变,而是变成了一种可见、可度量、可阻断的状态。没有图谱约束的速度会带来毫无遏制的混乱;以持续维护的意图图谱为锚点的速度,才是韧性软件工程的未来。
如需采取进一步行动,你可以考虑屏蔽此人和/或举报滥用行为。