Agent工坊

【Agent工坊】给AI Agent的代码上锁:用pre-commit+tree-sitter搭建自动化代码质量防线

AI Agent写出代码的速度是人的10倍,但它重复犯错的速度也是10倍——Merrilin团队用tree-sitter在pre-commit阶段拦截了8类高频错误,331个PR零事故。

前言

如果你正在用Claude Code、Cursor、Hermes Agent或任何AI编程工具做产品开发,你一定经历过这种撕裂感:Agent能在30秒内生成一个完整的功能模块,但同时,它也在30秒内复制了你在过去三个月里踩过的每一个坑。

raise HTTPException(...)except Exception: passcatch (error) { console.error(error) }——这些代码单独看都"能跑",但组合在一起就是一颗定时炸弹。

今天这篇文章,我们拆解Merrilin.ai团队的做法:他们用pre-commit + tree-sitter搭建了一套自动化防线,在AI Agent生成的代码被commit之前就拦截掉已知的坏模式。这不是"又一个lint工具"的科普——这是一套经过331个PR、81个数据库迁移文件验证的生产级方案。

读完这篇文章,你将获得:

  • 一套可复用的tree-sitter规则模板(8条核心规则)
  • 如何在你的项目中5分钟内接入这套防线
  • 为什么prompt约束对AI Agent的代码质量基本无效,以及什么才有效

为什么prompt指令挡不住AI Agent的坏代码

在进入实操之前,先理解一个关键认知:prompt约束对AI Agent的代码质量基本无效。

这不是说prompt engineering没有价值。问题在于AI Agent在生成长代码时的行为模式:当它面对一个需要50行以上的功能实现时,模型会进入"交付模式"——优先保证功能能跑通,而不是保证代码符合架构规范。

Merrilin团队在文章里说的很直白:

"There is a certain wonder in watching a machine produce so much working code so quickly. There is also a certain exhaustion in watching it rediscover the exact same bad ideas at scale."

—— 看着机器快速产出能跑的代码有一种奇妙的快感。看着它在大规模下不断"重新发现"一模一样的坏主意,又有一种深沉的疲惫。

这就是为什么他们从"prompt约束"转向了"代码级硬边界"。

核心方案:tree-sitter + pre-commit

pre-commit+tree-sitter防线架构

▲ 图1:pre-commit + tree-sitter防线架构流程

为什么是tree-sitter而不是grep或传统AST?

这是很多人会问的第一个问题。答案有三个层面:

1. grep是盲目的

grep只能做文本匹配。grep "except Exception" 会匹配到注释里的代码示例、字符串里的内容、以及确实需要捕获Exception的合法场景。你无法区分"这个except是否跟着rollback"、"这个catch是否在关键路径上"。

2. 传统AST太重

Python的ast模块、TypeScript的ts-morph可以解析语法树,但它们:

  • 需要完整的项目上下文(依赖安装、类型解析)
  • 速度慢(一个中型项目可能需要几十秒)
  • pre-commit阶段等不起

3. tree-sitter刚刚好

tree-sitter是一个增量解析器,它在以下维度做到了最佳平衡:

  • 速度:毫秒级解析单个文件
  • 容错:不完整的代码也能解析出部分AST
  • 跨语言:Python、TypeScript、TSX一套工具搞定
  • 精确性:能区分except HTTPException as e:except Exception: logger.exception(...)

架构总览

AI Agent生成代码

    ↓

git add

    ↓

pre-commit hook触发

    ↓

tree-sitter解析 Python/TS/TSX → AST

    ↓

遍历AST节点,匹配违规模式

    ↓

┌─ 通过 → git commit 继续

└─ 拒绝 → 打印违规行 + 原因 → 阻止commit

关键设计决策:在pre-commit阶段拦截,而非PR阶段。 理由很简单——PR review时坏模式已经"沉淀"在代码库中,修改的成本远高于在本地commit前拦截。

8条保命规则详解

AI Agent坏代码vs正确错误处理模式对比

▲ 图2:AI Agent坏代码 vs 正确错误处理模式对比

Merrilin团队的核心产品是AI阅读伴侣,所以他们有一条"宪法级"铁律:绝对不能打断用户的阅读体验。 围绕这条铁律,他们定义了8条tree-sitter规则。我们逐条拆解,给出可复用的实现思路。

规则1:数据库操作后不能"抓了异常继续跑"

# ❌ AI Agent最爱的坏模式

try:

    db.execute(update_query)

except Exception:

    logger.exception("something went wrong")

    # 继续执行——事务可能已经处于损坏状态

# ✅ 正确模式

try:

    db.execute(update_query)

except DatabaseError:

    db.rollback()

    raise ServiceUnavailableError("database operation failed")

tree-sitter实现思路

  • 检测try块中是否包含数据库操作(如db.executesession.querycursor.execute
  • 检查except块中是否缺少rollback()raise语句
  • 如果捕获了异常但既没有回滚也没有重新抛出 → 拒绝

规则2:SQLAlchemy异常捕获没有恢复边界

# ❌ AI Agent容易写出的模糊异常处理

try:

    user = db.query(User).filter_by(id=user_id).one()

except NoResultFound:

    return None # 调用方不知道是"用户不存在"还是"数据库挂了"

# ✅ 明确区分异常类型

try:

    user = db.query(User).filter_by(id=user_id).one()

except NoResultFound:

    raise UserNotFoundError(user_id)

except OperationalError:

    raise ServiceUnavailableError("database unavailable, retry later")

tree-sitter实现思路

  • 检测except子句捕获了SQLAlchemy异常但没有映射到业务异常
  • 强制要求至少有两种不同的except分支(区分"资源不存在"和"基础设施故障")

规则3:IntegrityError被静默吞掉

# ❌ 最危险的模式

try:

    db.add(new_record)

    db.commit()

except IntegrityError:

    pass # 数据丢了,但没人知道

# ✅ 转换为业务语义

try:

    db.add(new_record)

    db.commit()

except IntegrityError as e:

    db.rollback()

    if "duplicate key" in str(e):

        raise ConflictError("record already exists")

    raise ServiceUnavailableError("database constraint violation")

规则4:后台数据库错误没有回滚

# ❌ 后台任务里的裸异常

@celery.task

def process_order(order_id):

    try:

        order = db.query(Order).get(order_id)

        # ... 处理逻辑

    except Exception:

        # 没有rollback!数据库连接可能已损坏

        logger.error(f"failed to process {order_id}")

# ✅ 后台任务必须有显式的事务边界

@celery.task

def process_order(order_id):

    try:

        with db.begin() as transaction:

            order = db.query(Order).get(order_id)

            # ... 处理逻辑

    except Exception:

        logger.error(f"failed to process {order_id}, transaction rolled back")

        raise # 让Celery处理重试

规则5:读者关键路径上的数据库操作没有恢复机制

这需要你对项目的"关键路径"有明确定义。Merrilin的做法是:标记所有涉及阅读进度的代码路径为"reader-critical",然后检查这些路径上的数据库操作是否都有恢复逻辑。

# reader-critical 路径上的脆弱代码

def save_reading_progress(user_id, book_id, position):

    db.execute(update_progress_sql) # 失败了怎么办?用户进度丢失。

# 加固后

def save_reading_progress(user_id, book_id, position):

    try:

        db.execute(update_progress_sql)

    except DatabaseError:

        # 先写入本地缓存,后台重试

        cache.set(f"progress:{user_id}:{book_id}", position, ttl=86400)

        queue.enqueue(sync_reading_progress, user_id, book_id, position)

        # 不阻断用户——阅读继续

规则6:读者状态写入没有隔离

# ❌ 直接在当前事务中修改全局读状态

def mark_chapter_read(user_id, chapter_id):

    state = db.query(ReadingState).filter_by(user_id=user_id).first()

    state.last_chapter = chapter_id

    db.commit() # 如果commit失败,状态不一致

# ✅ 状态变更必须隔离

def mark_chapter_read(user_id, chapter_id):

    with db.begin_nested() as savepoint:

        state = db.query(ReadingState).filter_by(

            user_id=user_id

        ).with_for_update().first()

        state.last_chapter = chapter_id

        state.updated_at = func.now()

规则7:前端代码直接读取传输层错误细节

// ❌ AI Agent在前端常写的耦合代码

try {

    await api.startReading(bookId);

} catch (error) {

    // 直接读取axios响应的内部结构

    const message = error.response.data.detail;

    showError(message); // 后端改了错误格式,前端就炸

}

// ✅ 通过共享错误标准化层

try {

    await api.startReading(bookId);

} catch (error) {

    const normalizedError = normalizeApiError(error);

    // normalizeApiError负责将任何后端响应转为统一格式

    showError(normalizedError.message);

    if (normalizedError.code === 'SUBSCRIPTION_REQUIRED') {

        redirectToUpgrade();

    }

}

tree-sitter实现思路

  • 在TypeScript/TSX文件中检测error.response.data 的直接访问
  • 任何对error.response 内部结构的直接读取都必须通过normalizeApiError 包装

规则8:被吞掉的Promise拒绝和空catch块

// ❌ AI Agent会写出的各种静默失败

fetch(url).catch(console.error); // 失败了,日志有了,但UI不知道

try {

    await criticalOperation();

} catch (e) {

    // 空catch块——什么都没做

}

// ✅ 显式错误处理

try {

    await criticalOperation();

} catch (e) {

    captureException(e, { context: 'criticalOperation' });

    setOperationStatus('failed', normalizeError(e));

    // 确保UI表达了失败状态

}

tree-sitter实现思路

  • 检测catch块中只有console.error/console.log而没有状态更新或错误上报
  • 检测完全空的catch块(连注释都没有)
  • 检测.catch(console.error) 模式

接入指南:5分钟在你的项目中搭建

5步接入指南

▲ 图3:5步接入指南流程

第一步:安装依赖

# Python后端

pip install tree-sitter tree-sitter-language-pack

# 或者通过pre-commit管理

# .pre-commit-config.yaml

repos:

  - repo: local

    hooks:

      - id: error-handling-patterns

        name: Guard centralized error handling patterns

        entry: python scripts/check_error_handling_patterns.py

        language: system

        files: ^(backend/.*\.py|frontend/.*\.(ts|tsx))$

        stages: [pre-commit]

第二步:编写检查脚本框架

# scripts/check_error_handling_patterns.py

import sys

from pathlib import Path

from tree_sitter import Language, Parser

from tree_sitter_language_pack import get_language, get_parser

PY_LANG = get_language('python')

TS_LANG = get_language('typescript')

TSX_LANG = get_language('tsx')

def check_python_file(filepath: str) -> list[str]:

    """检查Python文件中的错误处理反模式"""

    issues = []

    parser = get_parser('python')

    tree = parser.parse(Path(filepath).read_bytes())

    # 规则1: except后没有rollback或raise

    issues.extend(check_db_exception_without_rollback(tree, filepath))

    # 规则2: 模糊的except Exception

    issues.extend(check_bare_except(tree, filepath))

    # 规则3: IntegrityError被静默吞掉

    issues.extend(check_silenced_integrity_error(tree, filepath))

    return issues

def check_db_exception_without_rollback(tree, filepath):

    """使用tree-sitter查询检测数据库异常处理缺陷"""

    issues = []

    # 核心逻辑:遍历try-except节点

    # 检测except块中是否有db操作但没有rollback

    query = PY_LANG.query("""

    (try_statement

      body: (block) @try_body

      (except_clause

        (block) @except_block))

    """)

    captures = query.captures(tree.root_node)

    # ... 具体检测逻辑

    return issues

if __name__ == '__main__':

    files = sys.argv[1:] or []

    all_issues = []

    for f in files:

        if f.endswith('.py'):

            all_issues.extend(check_python_file(f))

        elif f.endswith(('.ts', '.tsx')):

            all_issues.extend(check_typescript_file(f))

    if all_issues:

        for issue in all_issues:

            print(f"❌ {issue}", file=sys.stderr)

        sys.exit(1)

    else:

        print("✅ All error handling patterns are valid")

第三步:定义你的"关键路径"标记

Merrilin的做法是:在关键函数上使用装饰器标记,然后在tree-sitter检查时识别这些标记:

# 定义关键路径装饰器

def reader_critical(func):

    """标记此函数在读者关键路径上,需要额外的错误处理检查"""

    func._reader_critical = True

    return func

@reader_critical

def save_reading_progress(user_id, book_id, position):

    # tree-sitter检查会识别这个标记,并施加更严格的规则

    ...

第四步:配置pre-commit

# .pre-commit-config.yaml 完整示例

repos:

  - repo: pre-commit/pre-commit-hooks

    rev: v4.6.0

    hooks:

      - id: check-yaml

      - id: check-json

      - id: check-merge-conflict

      - id: detect-private-key

      - id: trailing-whitespace

      - id: end-of-file-fixer

  - repo: local

    hooks:

      - id: error-handling-patterns

        name: 🔒 Guard error handling patterns

        entry: python scripts/check_error_handling_patterns.py

        language: system

        files: ^(backend/app/.*\.py|apps/(web|mobile)/src/.*\.(ts|tsx))$

        stages: [pre-commit]

        pass_filenames: true

      - id: migration-naming

        name: 🔒 Enforce Alembic migration naming

        entry: python scripts/check_migration_naming.py

        language: system

        files: ^backend/alembic/versions/.*\.py$

        stages: [pre-commit]

第五步:团队推广

最重要的不是技术实现,而是让团队理解"为什么prompt不够":

  1. 做一次演示:让AI Agent生成一个包含100行代码的功能,然后跑检查脚本——通常至少能拦截2-3个问题
  2. 建立"规则贡献"文化:任何人发现Agent反复犯同一类错误 → 封装成一条tree-sitter规则 → PR进来
  3. 渐进式部署:前两周只告警不阻断(stages: [pre-commit]verbose: true),等团队适应后再开启阻断

为什么这套方案比AI Code Review更好

你可能会问:已经有CodeRabbit、GitHub Copilot Code Review这些AI审查工具了,为什么还要手动写tree-sitter规则?

成本:AI Code Review按PR收费或消耗token配额。如果你的Agent每天提交10-20个commit,AI审查的成本会快速累积。而pre-commit + tree-sitter是本地运行、零成本、毫秒级

反馈速度:AI审查需要等待PR创建→分配审查→返回结果。pre-commit在git commit时就立即给出反馈。从"写完代码"到"发现问题"的延迟从分钟级降到毫秒级。

确定性:AI审查有随机性——同一个模式昨天能被检测到,今天不一定。tree-sitter规则是确定性的——匹配就是匹配,不匹配就是不匹配。

边界清晰:你知道哪些规则是"硬性禁止"的。AI审查的边界模糊,有时会标记出"可能有问题"但实际没问题的代码,产生噪音。

常见踩坑与排障

坑1:tree-sitter解析失败但没报错

症状:某些文件静默跳过,检查结果总是PASS,但实际上文件中存在违规模式。

原因:tree-sitter对语法错误的文件会生成不完整的AST,某些节点缺失导致查询匹配不到。

解决:在检查脚本开头加一句文件语法校验:

if tree.root_node.has_error:

    print(f"⚠️ {filepath}: syntax error, skipping tree-sitter checks")

    return [] # 不要阻断,让lint工具处理语法问题

坑2:规则过于严格导致正常代码被拦截

症状:开发者的合法代码(如确实需要捕获所有异常的场景)被pre-commit拦截。

解决:添加抑制注释机制,Merrilin的做法是:

try:

    risky_operation()

except Exception as e:

    logger.exception("unknown error") # noqa: ERR-001

    # ↑ ERR-001 是"禁止裸except"规则的编号

    # 这告诉checker:"我知道我在做什么,这条规则放行"

坑3:数据库迁移文件被Agent写乱

症状:AI Agent看到大量的迁移文件后,开始"发明"自己的编号规则,如手动创建065_add_new_table.py

解决:添加额外的迁移文件命名检查:

# scripts/check_migration_naming.py

import re

from pathlib import Path

MIGRATION_DIR = Path("backend/alembic/versions")

def check_migrations():

    issues = []

    for f in MIGRATION_DIR.glob("*.py"):

        # 禁止手动编号的迁移文件

        if re.match(r'^\d{3}_', f.name):

            issues.append(

                f"{f.name}: 禁止手动编号的迁移文件。"

                f"请使用 alembic revision --autogenerate -m '描述' 生成"

            )

        # 禁止不是通过alembic创建的文件

        content = f.read_text()

        if "Create Date:" not in content:

            issues.append(

                f"{f.name}: 缺少Alembic生成标记。"

                f"请用alembic revision命令创建迁移文件"

            )

    return issues

坑4:性能问题

症状:pre-commit检查越来越慢,每次commit都要等几秒。

解决

  1. 使用--changed-lines只检查变更的代码行(而非整个文件)
  2. 对于大型仓库,考虑按模块拆分检查规则
  3. 如果检查时间超过2秒,使用concurrent.futures.ProcessPoolExecutor并行执行

加分项:与其他Agent工具的协同

这套防线不是孤立存在的。它可以和你已有的AI开发工具形成协同:

  • Claude Code + pre-commit:Claude Code在生成代码后执行git add会触发pre-commit。如果检查失败,Claude Code会看到错误输出并尝试修复——形成"生成→检查→修复"的自动循环。
  • Hermes Agent + pre-commit:在Hermes的cron任务中配置代码生成+pre-commit检查的流水线,确保自动生成的代码不会污染仓库。
  • GitHub Actions + pre-commit:CI中再次运行同样的检查作为双重保险(pre-commit可能会被--no-verify跳过)。

总结

给AI Agent的代码上锁,不是不信任它——恰恰相反,正是因为充分信任它的产出速度,才需要用确定性更强的防线来匹配。

核心要点回顾:

  1. prompt约束不够硬——AI Agent总是会在生成长代码时走捷径
  2. tree-sitter + pre-commit——毫秒级、零成本、确定性的代码质量防线
  3. 8条核心规则——从数据库事务安全到前端错误处理,覆盖AI Agent最常见的坏模式
  4. 5分钟接入——pip install → 写脚本 → 配置pre-commit → 团队推广
  5. 渐进式部署——先告警、再阻断,让团队适应期有缓冲

如果你也在用AI Agent做生产级开发,不妨今天就在你的项目里加一条规则试试——就从禁止except Exception: pass开始。


#AI创业 #Agent工坊 #AI编程 #代码质量 #pre-commit

本文由AI辅助创作,经人工审核编辑发布