前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
返回 AI 情报前线
All News · 全部资讯10008
  • 用分区标记和哈希防止 AI 越界改文档
  • Agent 失败排查:先查验收与重试机制
  • 把 TimesFM 时序预测搬进浏览器
  • 接收外部PDF的服务应优先修补lopdf
  • TypeScript MCP迁移:SDK升级还需选对入口
  • 禁止重复片段为何会破坏模型JSON输出
  • 用普通函数搭建 Agent,再按需引入框架
  • 用图记忆串起生产故障排查线索
  • OpenAI 披露模型越权取数且隐瞒行为
  • 链接预览串图:用内容哈希修正缓存键
  • 让偶发测试稳定失败,再交给编程智能体
  • 用 Playwright 验证网页提供的智能体动作
  • 用棋力引擎约束模型,减少复盘幻觉
  • 用异步模拟定位连接池耗尽与超时
  • 把Agent提示词写成可执行的行为规范
  • 用 .NET 为小型网站搭建 MCP 检索入口
  • 网站内容喂给模型前,先做好抓取取舍
  • 用图数据库追查参考文献背后的撤稿链
  • DeepSeek 更新开放权重模型并开放搜索接口
  • 研究发现 AI 工作汇报频繁遗漏负面结果
  • DeepSeek 新检查点称 DeepSWE 达 77 分
  • 大模型接口账单怎么估才靠谱
  • 给 Claude Code 换模型的配置与代价
  • DeepSeek 更新开源模型,强化整项目生成
  • Claude 借自动评估迭代优化量子电路
  • 阶跃开放 Step 5 预览版权重,支持百万上下文
  • Android 技能库补齐编程智能体短板
  • 部署 Kandinsky 视频生成前先算吞吐
  • 生成接口重试如何避免重复扣额度
  • OpenAI dots 据报可在云端持续执行任务
  • Meta 据报开放 Muse Spark 模型权重
  • 已加载 31 / 10008
8.0
热点
AI SCORE
技术实践2026-10-18 07:17

用分区标记和哈希防止 AI 越界改文档

dev.to · AI#文档工程#持续集成#AI协作
Editor brief · 编辑速览

在 Markdown 内标记模型可编辑区与人工负责区,通过 Python 哈希检查在提交时拦截越界修改。该机制约束内容归属,不验证文字质量或事实正确性。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

模型可以起草什么、哪些内容必须由人类负责,这条边界只有在机器可读、并且每次提交都经过检查时,才能真正守住。本文把这条边界直接写进 Markdown 文件,再加上一道小巧的 Python 检查关卡:一旦起草行为越界,就让 pull request 检查失败。这道关卡不评价写作质量,也不判断说法是否属实;它只约束内容归属。当草稿涌入的速度超过审阅速度时,审阅流程最先丢掉的,往往就是这项约束。下面的所有操作都是普通的文件解析,因此这套方法适用于任何起草模型和任何编辑器。

要防止哪种失误

大多数文档流水线把整个页面作为一个审阅单元。因此,你让模型精简一段摘要,它也可能顺手改掉值班轮换安排和弃用警告。审阅者随后把整个 diff 当作一个整体来读,对熟悉的段落点头通过,对不熟悉的段落一带而过。结果就是:某句话看起来已经定稿,所在章节却没有明确的负责人,三周后还被支持工程师引用了。区域标记把审阅单元从页面缩小到段落,而对人类负责的段落计算 hash,则把内容归属变成了一项无需审阅者在场也能执行的检查。

两类区域,各守一条规则

草稿区域的规则刻意宽松,因为起草的价值就在这里。人类区域的规则刻意严格,因为这套工作流要抓住的,正是发生在这里的悄然修改。检查关卡只比较标识符和 hash,从不解析正文,因此开销足够低,可以在每次提交时运行。

第 1 步:在页面中标记区域

标记使用 HTML 注释,渲染后的页面中看不到,但原始 diff 中仍然清晰可见。每组段落使用一个区域;如果内容归属不明显,就把负责人写进 id;区域之间绝不能嵌套。

# Rate limits

<!-- scope:draft id=summary -->
The API returns 429 responses when a client exceeds its window.
<!-- /scope:draft -->

<!-- scope:human id=limits-owner-sre -->
The current window is 60 seconds and is configured per key.
Contact #platform-sre before changing it.
<!-- /scope:human -->

有三个细节很重要。id 是稳定的标识符,不是标题,因此重命名标题不会让清单失效。人类区域应该只包含需要有人负责的陈述,而不是页面上的每一句话。标记行不参与 hash 计算,因此修改 id 时,不必把它当作内容变化。

第 2 步:只对人类负责的文本计算 hash

直接对原始字节计算 hash,会被行尾空白和编辑器自动折行干扰,所以要先规范化,再计算摘要。去掉每一行的行尾空格,删除开头和结尾的空行,然后用单个换行符连接。不要合并内部空格,也不要清理围栏代码块内部的空白,因为值的变化可能就藏在那里。

# tools/scopecheck.py (part 1: the hashing core)
import hashlib

def normalize(lines):
    kept = [line.rstrip() for line in lines]
    while kept and not kept[0].strip():
        kept.pop(0)
    while kept and not kept[-1].strip():
        kept.pop()
    return '\n'.join(kept)

def digest(lines):
    body = normalize(lines).encode('utf-8')
    return 'sha256:' + hashlib.sha256(body).hexdigest()[:16]

这里将摘要截取为 16 个字符就足够了,因为比较范围很小,而且仅限本地。这不是安全控制,也不应该被当作段落可信的证明。它唯一的职责,是检测自清单写入以来,受保护标记内的字节是否发生了变化。

第 3 步:解析、比较,并以非零状态码退出

检查器遍历文档目录树,逐行解析标记,并在开始比较前拒绝格式错误的标记。区域嵌套、区域未闭合,或 id 重复,都会触发硬错误,退出码为 2,因为一道靠猜测继续运行的关卡,还不如直接停下来。

# tools/scopecheck.py (part 2: the checker, same file as part 1)
import argparse
import json
import re
import sys
from pathlib import Path

MARKER = re.compile(
    r'^<!--\s*(?P<close>/)?scope:(?P<kind>draft|human)\s+id=(?P<rid>[A-Za-z0-9_.-]+)\s*-->\s*$'
)

class BadMarkup(Exception):
    pass

def parse(path):
    regions, stack = {}, []
    for lineno, line in enumerate(path.read_text(encoding='utf-8').splitlines(), 1):
        found = MARKER.match(line)
        if found is None:
            if stack:
                stack[-1]['lines'].append(line)
            continue
        rid, kind = found.group('rid'), found.group('kind')
        if found.group('close') is None:
            if stack:
                raise BadMarkup(f'{path}:{lineno}: nested region in {stack[-1]["rid"]}')
            stack.append({'rid': rid, 'kind': kind, 'line': lineno, 'lines': []})
            continue
        if not stack:
            raise BadMarkup(f'{path}:{lineno}: close marker without an open marker')
        opened = stack.pop()
        if (opened['rid'], opened['kind']) != (rid, kind):
            raise BadMarkup(f'{path}:{lineno}: close marker does not match {opened["rid"]}')
        if rid in regions:
            raise BadMarkup(f'{path}:{lineno}: duplicate region id {rid}')
        regions[rid] = {'kind': kind, 'line': opened['line'], 'digest': digest(opened['lines'])}
    if stack:
        raise BadMarkup(f'{path}: unclosed region {stack[-1]["rid"]}')
    return regions

def scan(root):
    state = {}
    for path in sorted(root.rglob('*.md')):
        regions = parse(path)
        state[str(path.relative_to(root))] = {
            'draft': sorted(r for r, v in regions.items() if v['kind'] == 'draft'),
            'human': {r: v['digest'] for r, v in regions.items() if v['kind'] == 'human'},
        }
    return state

def compare(baseline, current):
    problems = []
    for name, expected in baseline.items():
        actual = current.get(name)
        if actual is None:
            problems.append(f'{name}: file is gone but the manifest still lists it')
            continue
        if actual['draft'] != expected['draft']:
            problems.append(f'{name}: draft region ids changed')
        for rid, want in expected['human'].items():
            got = actual['human'].get(rid)
            if got is None:
                problems.append(f'{name}: human region {rid} is missing')
            elif got != want:
                problems.append(f'{name}: human region {rid} was edited ({want} -> {got})')
        for rid in actual['human']:
            if rid not in expected['human']:
                problems.append(f'{name}: human region {rid} is not in the manifest')
    for name in current:
        if name not in baseline:
            problems.append(f'{name}: has regions but no manifest entry, run --update')
    return problems

def main(argv=None):
    ap = argparse.ArgumentParser()
    ap.add_argument('root', type=Path)
    ap.add_argument('--manifest', type=Path, default=Path('.docscope/manifest.json'))
    ap.add_argument('--update', action='store_true')
    ap.add_argument('--reviewed-by', default='')
    args = ap.parse_args(argv)
    try:
        current = scan(args.root)
    except BadMarkup as exc:
        print(f'scopecheck: {exc}', file=sys.stderr)
        return 2
    if args.update:
        if not args.reviewed_by:
            print('scopecheck: --update requires --reviewed-by', file=sys.stderr)
            return 2
        args.manifest.parent.mkdir(parents=True, exist_ok=True)
        payload = json.dumps(current, indent=2, sort_keys=True) + '\n'
        args.manifest.write_text(payload, encoding='utf-8')
        print(f'scopecheck: manifest written, reviewed by {args.reviewed_by}')
        return 0
    if args.manifest.exists():
        baseline = json.loads(args.manifest.read_text(encoding='utf-8'))
    else:
        baseline = {}
    problems = compare(baseline, current)
    for problem in problems:
        print(f'sco

这段脚本中有两个设计选择值得解释。草稿区域按 id 跟踪,而不是按 hash 跟踪,因此日常改写不会导致清单频繁变化,审阅者只会看到人类负责内容的变化。另外,--update 在没有 --reviewed-by 时会拒绝运行,因此每次刷新基线都会记录一个人,让清单成为一份轻量的审批日志,而不是缓存。

第 4 步:在审阅 diff 之前运行检查

在本地通过 pre-commit hook 运行检查关卡,并在 CI 中再运行一次,避免因为缺少 hook 而让有问题的修改被合并。

python tools/scopecheck.py docs --manifest .docscope/manifest.json
# .git/hooks/pre-commit
#!/bin/sh
python tools/scopecheck.py docs --manifest .docscope/manifest.json || {
  echo 'Human-owned doc region changed. Review it, then re-run with:'
  echo '  python tools/scopecheck.py docs --update --reviewed-by "$(git config user.name)"'
  exit 1
}
- name: human-owned doc regions unchanged
  run: python tools/scopecheck.py docs --manifest .docscope/manifest.json

当有人确实需要修改受保护的段落时,流程是:先编辑,再用自己的名字运行更新命令,最后把页面和清单一起提交。清单的 diff 会准确显示哪些受保护区域发生了变化,审阅它比审阅整个重新生成的页面要省事得多。

第 5 步:先测试关卡,再信任它

一道从未失败过的关卡,等于从未经过测试。下面四个测试覆盖了关键情况,运行时间远不到一秒。

# tests/test_scopecheck.py
import pytest

import scopecheck

PAGE = '''# Rate limits

<!-- scope:draft id=summary -->
The API returns 429 responses when a client exceeds its window.
<!-- /scope:draft -->

<!-- scope:human id=limits-owner-sre -->
The current window is 60 seconds and is configured per key.
<!-- /scope:human -->
'''

def build(tmp_path, name, text):
    doc = tmp_path / name / 'docs' / 'rate-limits.md'
    doc.parent.mkdir(parents=True)
    doc.write_text(text, encoding='utf-8')
    return doc.parent

def test_draft_rewrite_is_allowed(tmp_path):
    before = scopecheck.scan(build(tmp_path, 'a', PAGE))
    after = scopecheck.scan(build(tmp_path, 'b', PAGE.replace('429 responses', 'HTTP 429')))
    assert scopecheck.compare(before, after) == []

def test_human_edit_is_flagged(tmp_path):
    before = scopecheck.scan(build(tmp_path, 'a', PAGE))
    after = scopecheck.scan(build(tmp_path, 'b', PAGE.replace('60 seconds', '30 seconds')))
    assert 'edited' in scopecheck.compare(before, after)[0]

def test_removed_human_region_is_flagged(tmp_path):
    before = scopecheck.scan(build(tmp_path, 'a', PAGE))
    stripped = PAGE.replace('<!-- scope:human id=limits-owner-sre -->\n', '')
    stripped = stripped.replace('<!-- /scope:human -->', '')
    after = scopecheck.scan(build(tmp_path, 'b', stripped))
    assert scopecheck.compare(before, after)

def test_nested_markers_are_rejected(tmp_path):
    bad = '<!-- scope:draft id=a -->\n<!-- scope:draft id=b -->\n<!-- /scope:draft -->\n'
    with pytest.raises(scopecheck.BadMarkup):
        scopecheck.scan(build(tmp_path, 'a', bad))

使用 pytest -q tests/test_scopecheck.py 运行测试。如果在人类内容被修改、清单却没有更新的情况下,检查仍然通过,那么这道关卡就只是摆设,暂时还不能依赖它。

起草环节放在哪里

只有确实有东西在区域内起草,区域标记才有意义,而起草任务也需要一个运行的地方。按照运营方的介绍,MonkeyCode 提供的免费模型访问和免费服务器选项,分别解决了这两个问题:模型访问用于按照固定 prompt,仅起草 scope:draft 区域的内容;服务器选项则用于运行批处理任务,不必让工作站一直开着。prompt 刻意写得平淡直接,因为定义边界的是检查关卡,而不是指令。

You may rewrite text between scope:draft markers only.
Return the full file. Do not add, remove, or rename any marker.
Do not touch text between scope:human markers.

利益披露:本文是 MonkeyCode 产品推广活动的一部分。如果你想在不配置计费的情况下试用起草环节,可以从免费模型访问开始,让检查关卡快速告诉你,草稿是否始终留在各自的区域内。

这道关卡不能证明什么

它证明的是人类区域的字节完全一致,而不是内容正确;一段没人碰过的过时内容,也会顺利通过。

它无法阻止草稿区域内的语义漂移,因此,模型改写的所有内容仍然需要逐项核查其中的说法。

只有通过草稿 id 集合才能发现标记被移除,因此,对整个文件进行一致性重构的改写不在它的处理范围内。

它不检查扫描根目录之外的文件,也不涉及 CMS 中的内容或翻译版本;翻译需要为每种语言单独维护清单。

哪些团队不该采用这套方法

只有一位文档作者的团队收益不大,因为 hash 关卡只是重复了一项原本每次都会进行的审阅。文档存放在产品 CMS 中的仓库,应该先理顺发布流程,再添加标记。而那些受保护章节每周都要修改的团队,会逐渐把 --update 变成条件反射;到了这一步,清单就成了走过场的盖章,再也没有实际意义。

先标记两个页面,不要一上来就标记二十个,并选择那些确实需要明确负责人的章节。

生成清单时,把 --reviewed-by 设置为真实审阅者的姓名。

添加 CI 步骤,并刻意修改人类区域中的内容,确认它会失败。

写好这四个测试,运行通过之后,再启用起草任务。

如果还想采取进一步行动,可以考虑屏蔽此人和/或举报滥用行为。

Original source

本文由 AI 翻译整理自 dev.to · AI,原文版权归原作者所有。

阅读英文原文
下一篇
Agent 失败排查:先查验收与重试机制