前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
返回 AI 情报前线
All News · 全部资讯8611
  • 自托管Llama部署成本全解析:GPU之外容易被忽视的隐性开销
  • Builder.io开源Agent-Native框架:让人与AI共享同一操作层
  • 让AI编程代理在中断后从断点恢复:session化执行轨迹方案
  • CI如何验证AI代理真的跑了测试而不是伪造结果
  • AI产品定价数学:固定费率如何悄悄亏损
  • ZCode事件:AI编程工具静默上传全部Git历史
  • Anthropic自评:R&D自动化率26%,但完全自主为0
  • 阿里Qwen-Image-2.1:70亿参数开源图像生成拳打闭源模型
  • OWASP CRS 规则引擎:给 LLM 和 MCP 加上 WAF
  • 智谱 MaaS 上线数据不留存机制,可申请开通
  • 如何界定可完成的AI工程范围
  • Cursor中直接查询技术文档的MCP方案
  • AI Agent的过度自信陷阱
  • Cursor与.NET周报:七个实战规则
  • Qwen-Image-2.1开源:7B参数兼顾生图与编辑
  • 阿里开源 Qwen-Image-2.1:7B 参数兼顾透明图生成与多图编辑
  • 免费模型Trace不能支持的五个评估主张
  • AI重构前的副作用冻结术:先录 Ledger 再动格式
  • Agent能改Oracle则Green Build不足为信:独立检查三原则
  • AI编程导致代码质量下降?根子在质量管理
  • AI连接数据库必须先脱敏:PII保护实战指南
  • Anthropic与埃森哲20亿美元共建AI安全评估体系
  • Copilot CLI恢复检查点导致1GB未跟踪文件被删
  • AI代码审查升级:结果直发PR评论,淘汰复制粘贴
  • Higgsfield:开源万亿参数模型分布式训练框架
  • LlamaIndex多Agent工作流:事件驱动共享状态编排
  • 用AI从45条Ruff PR评论中挖掘团队隐式代码规范
  • LLM Agent工具授权最佳实践:五步检查清单
  • Agent补丁评分应只看其未写的断言
  • 2026年9月 Ollama 编程模型实测推荐
  • 国产大模型价格实测:GPT-5 三到八倍溢价
  • Codex-X:OpenAI Codex桌面端可视化综合管理工具
  • Docling:支持复杂PDF结构的文档解析库,集成主流AI框架
  • NVIDIA PAIR:开源本地多Agent推理路由,支持Ollama/LM Studio
  • 阿里Qwen发布Qwen3.8-LiveTranslate:60语言实时翻译,延迟仅2.3秒
  • Supermemory:AI记忆与上下文引擎开源实现
  • OpenSpec:AI编程时代的规格驱动开发框架
  • MCP Agent工具集成测试实战指南
  • Claude Code 在 Zed 中的 ACP 协议传输机制实测
  • Mubayyin:基于结构化知识库做 AI 发布决策的智能体
  • 阶跃发布 Step 5 Preview:开源模型前三,单任务成本仅 Claude Opus 5 的 1/8
  • OpenClaw 2026.9.5:原子更新、插件热重载、对话分享与GPT Live扩展
  • Jev:返回类型化校准决策的System One模型
  • Gemini越狱事件:测试中入侵三家公司,Google隐瞒四月
  • Qwen3.8-Omni-Flash:匹敌Gemini多模态能力,价格更低
  • SpaceX 工程师月均 2000 条 PR 背后的验证工程实践
  • Unity发布官方插件:让Claude Code和OpenAI Codex使用最新教程
  • AI机器人安全基准:GPT-6和Claude让机械臂变杀手
  • 已加载 48 / 8611
8.0
热点
AI SCORE
编程提效2026-09-20 20:08

AI重构前的副作用冻结术:先录 Ledger 再动格式

dev.to · AI#AI重构#测试#工程实践
Editor brief · 编辑速览

AI重写代码前应先冻结副作用记录(stdout、sidecar文件、累加器),再小步提取格式化工具,防止隐藏bug被掩盖。

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

完整中文译文

模型可以在几秒内重写一个凌乱的模块。但这种额外的速度并不是合并的信号。先冻结 stdout、sidecar 文件和可变累加器,再提取一个 formatter。

在这些可观测对象被固定之后,再提取一个 formatter。把其余未测试的树保持原样。

这个工作流是一道关卡,不是氛围检查。它适用于将 I/O 与字符串构建混在一起的未测试 Python 代码。它不适用于新设计或 broad API 重构。

为什么"直接清理"在凌乱的树上会失败

未测试的树隐藏着顺序、编码和变更bug。Formatter 的变更可能改变 JSON key 的顺序。Helper 提取可能跳过必需的 sidecar 写入。

排序清理可能打乱稳定的发票标识符顺序。AI 辅助的 diff 使这种失败变得更廉价,从而更容易被忽略。

生成的补丁通常看起来流畅且完整。表征测试通常还不存在。审阅者于是批准了语气而非精确的字节。

对策是一份记录好的 side-effect ledger。记录当前代码已经发出的每个可观测对象。在恰好一个小变更之后断言该 ledger。

当前的 AI 编码 thread 奖励看起来自信的输出。它们很少奖励冻结的字节而非流畅的文章。本文将那种自信视为不可信的输入。

测试用的凌乱模块

将以下模块视为 fixture,而非生产历史。它混合了解析、变更、打印和 sidecar 文件。这种混合才是真正的 bug 表面。

# invoice_summary.py
from __future__ import annotations

import json
from pathlib import Path
from typing import Any

TOTALS: dict[str, float] = {}

def run(data_dir: str, out_dir: str) -> dict[str, Any]:
    root = Path(data_dir)
    dest = Path(out_dir)
    dest.mkdir(parents=True, exist_ok=True)
    rows: list[dict[str, Any]] = []
    for path in root.glob("*.json"):
        payload = json.loads(path.read_text(encoding="utf-8"))
        code = str(payload.get("code") or path.stem)
        amount = float(payload.get("amount") or 0)
        TOTALS[code] = TOTALS.get(code, 0.0) + amount
        rows.append({"code": code, "amount": amount, "file": path.name})
    rows.sort(key=lambda r: (r["code"], r["file"]))
    lines = []
    for row in rows:
        lines.append(f"{row['code']}:{row['amount']:.2f}")
    text = "\n".join(lines) + "\n"
    print(text, end="")
    (dest / "summary.txt").write_text(text, encoding="utf-8")
    (dest / "summary.ok").write_bytes(b"ok\n")
    return {"count": len(rows), "codes": [r["code"] for r in rows]}

注意模块作用域中进程级别的 TOTALS 映射。注意 glob 顺序馈入后续的显式排序。注意 print 输出加两个磁盘 artifact。

模型会想要一次性简化所有这些。拒绝任何在第一轮就那么宽的补丁。下一个 diff 只涉及一个纯 helper。

Artifact:side-effect ledger 测试

构建一个冻结四个独立可观测对象的 ledger。不要 mock Path,也不要 stub print。使用临时目录并捕获 stdout。

# test_invoice_summary_ledger.py
from __future__ import annotations

import io
import json
import sys
from pathlib import Path

import invoice_summary as mod

def write_fixture(root: Path) -> None:
    (root / "b.json").write_text(
        json.dumps({"code": "B-9", "amount": 10}), encoding="utf-8"
    )
    (root / "a.json").write_text(
        json.dumps({"code": "A-1", "amount": 2.5}), encoding="utf-8"
    )
    (root / "a2.json").write_text(
        json.dumps({"code": "A-1", "amount": 0.5}), encoding="utf-8"
    )

def capture_run(data: Path, out: Path) -> tuple[dict, str]:
    buf = io.StringIO()
    old = sys.stdout
    sys.stdout = buf
    try:
        result = mod.run(str(data), str(out))
    finally:
        sys.stdout = old
    return result, buf.getvalue()

def test_side_effect_ledger(tmp_path: Path) -> None:
    mod.TOTALS.clear()
    data = tmp_path / "in"
    out = tmp_path / "out"
    data.mkdir()
    write_fixture(data)

    result, stdout = capture_run(data, out)

    ledger = {
        "stdout": stdout,
        "summary": (out / "summary.txt").read_text(encoding="utf-8"),
        "ok_bytes": (out / "summary.ok").read_bytes(),
        "totals": dict(mod.TOTALS),
        "result": result,
    }
    assert ledger["stdout"] == "A-1:2.50\nA-1:0.50\nB-9:10.00\n"
    assert ledger["summary"] == ledger["stdout"]
    assert ledger["ok_bytes"] == b"ok\n"
    assert ledger["totals"] == {"A-1": 3.0, "B-9": 10.0}
    assert ledger["result"] == {
        "count": 3,
        "codes": ["A-1", "A-1", "B-9"],
    }

在任何提取尝试开始之前运行该 ledger。

python -m pytest test_invoice_summary_ledger.py -q

该 ledger 是此文件的合并契约。重复的 code 在行列表中保持重复。Totals 仍然合并而 sidecar 字节保持 ASCII。

Formatter 提取不能"修复"那些行为。重复的发票行是当前的产品行为。静默清理将是一次未审阅的语义变更。

决策表:下一个 diff 可以触碰什么

将此表用作变更预算。在发送模型 prompt 之前阅读每一行。

一个允许变更的行就是全部预算。两个 helper 提取属于后续 pull request。注释只允许放在新函数旁边。

按照严格顺序遵循以下七个步骤。

首先,将凌乱的文件复制到一个专用分支上。

用微小的显式 fixture 添加 ledger 测试。

运行 pytest 并在当前字节上确认绿色 baseline。

编写一个禁止额外编辑的单提取 prompt。

将模型 diff 应用到第二个工作树上。

重新运行 ledger 并拒绝 stdout 或 sidecar 漂移。

只合并 helper 加 ledger 测试。

第四步是免费 coding 模型可以帮忙的地方。它不能替代第三步或第六步。本地 pytest 仍然是唯一的合并 oracle。

保持变更小的 Prompt

将此 prompt 标记为暂未执行的指导。仅在 ledger 变绿之后粘贴。

Extract a pure function format_rows(rows) -> str from run().
Do not rename run.
Do not clear TOTALS.
Do not change print, glob, or sidecar writes.
Do not dedupe codes.
Keep the sort key (code, file).
Keep two-decimal amounts and a trailing newline.
Return a unified diff for invoice_summary.py only.

该 prompt 是变更范围周围的栅栏。模型会在没有显式栅栏的情况下扩展清理。如果 diff 也编辑了测试文件,则丢弃该补丁。

免费模型访问应该放在哪里

表征 loop 需要一个一次性的提议环境。本地 pytest 应该留在你自己的机器上。模型只负责提议 formatter 提取。

披露:本文是 MonkeyCode 产品推广的一部分。

MonkeyCode 提供免费模型访问和免费服务器选项。将这对用于提议步骤,而非 ledger 执行。将 secrets 远离该服务器并保持 fixture 是合成的。

如果 diff 编辑了超过 format_rows 的内容,则丢弃该补丁。用相同的栅栏重新 prompt,不涉及额外文件。不要因为推理是免费的就接受宽泛清理。

如果你尝试免费模型访问,将 ledger 保持为合并关卡。不要将免费服务器当作测试运行器的替代品。

预期的提取形态

最小的安全变更如下所示。任何更大的变更都是不同的任务和不同的审阅。

def format_rows(rows: list[dict[str, Any]]) -> str:
    lines = [f"{row['code']}:{row['amount']:.2f}" for row in rows]
    return "\n".join(lines) + "\n"

def run(data_dir: str, out_dir: str) -> dict[str, Any]:
    root = Path(data_dir)
    dest = Path(out_dir)
    dest.mkdir(parents=True, exist_ok=True)
    rows: list[dict[str, Any]] = []
    for path in root.glob("*.json"):
        payload = json.loads(path.read_text(encoding="utf-8"))
        code = str(payload.get("code") or path.stem)
        amount = float(payload.get("amount") or 0)
        TOTALS[code] = TOTALS.get(code, 0.0) + amount
        rows.append({"code": code, "amount": amount, "file": path.name})
    rows.sort(key=lambda r: (r["code"], r["file"]))
    text = format_rows(rows)
    print(text, end="")
    (dest / "summary.txt").write_text(text, encoding="utf-8")
    (dest / "summary.ok").write_bytes(b"ok\n")
    return {"count": len(rows), "codes": [r["code"] for r in rows]}

在补丁干净应用后检查三个事实。format_rows 必须保持为没有 I/O 的纯函数。run 必须仍然变更 TOTALS 并写入两个文件。

立即在打了补丁的树上重新运行 ledger。匹配 stdout 在提取后不是可选项。匹配 sidecar 字节在这里也不是可选项。

这些失败在模型 diff 中经常出现。在重试之前将每一个映射到 ledger 断言。

去重的 codes:两个 A-1 行必须留在 stdout 中。

浮点漂移:金额保持两位小数,包括 10.00。

换行漂移:连接的文本保持尾部换行符。

Sidecar 漂移:summary.ok 保持恰好 ok 加换行符。

全局清理:TOTALS 不能在提取内部重置。

排序漂移:顺序保持按 (code, file),而非 glob 遍历。

每个失败都是字节不匹配,而非风格争论。在更新栅栏后用更严格的 prompt 重试。不要放松 ledger 来让模型通过。

此方法根本不证明功能正确性。它只证明当前字节保持不变。Fixture 中错误的 totals 在提取后仍然是错误的。

Ledger 不固定挂钟时间。它不固定排序前的 glob 顺序。它不固定源 fixture 内部的 JSON key 顺序。

不要将此用于安全审阅。不要将此用于 TOTALS 上的并发写入。不要将绿色 ledger 视为重写包的许可证。

网络化发票源不在此范围内。数据库事务和多进程累加器也是如此。这些需要不同于 sidecar 文件的 oracle。

谁应该跳过此方法

如果模块已经有真实的契约测试,则跳过此方法。如果变更是语义修复而非提取,则跳过。如果 sidecar 是活锁文件,则跳过。

如果你不能在本地树上运行 pytest,则跳过。远程模型不是你的测试运行器。如果生产 secrets 不能被降级为 fixture,则跳过。

对于无法廉价快照的二进制格式,跳过。当法律输出必须在同一补丁中变更时跳过。只有在新测试下才能将语义修复与提取结合。

从 ledger 开始,然后提取一个纯函数。在任何合并之前重新运行四个可观测对象。那个序列就是此文件的全部方法。

免费模型可以起草 helper 文本。Ledger 仍然决定 diff 是否可以落地。当可以的时候,将提议和 oracle 放在不同的机器上。

Original source

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

阅读英文原文
上一篇
免费模型Trace不能支持的五个评估主张
下一篇
Agent能改Oracle则Green Build不足为信:独立检查三原则