摘要:当 AI 代理批量改写 Excel 工作簿成为常态,月底结账面临的核心问题不再是“算得对不对”,而是“这一版是谁改的”。本文以“版本指纹”为贯穿主线,把版本标记拆成文件层、单元格层、审计层三级可验证结构,给出基于 OOXML 自定义文档属性、批注签名、隐藏审计页与哈希清单的完整落地方案。文中提供可直接运行的 Python(openpyxl)、PowerShell 与 VBA 代码,覆盖 Power Query 刷新、共享工作簿冲突、审计留痕与回滚验证,并附排错清单与验证方法,帮助财务与数据团队在结账前把“人工版/机器版”的判定从经验变成可复核的证据链。
目录
一、问题界定:为什么“没有版本标记”是结账事故的温床
先把场景说清楚。典型情况是:财务共享中心或业务分析团队维护一套月度工作簿,包含总账余额表、往来对账表、费用分摊表和几张贴在报表上的辅助表。月初到月中,人工会计会做科目调整、补充备注、修正公式引用;与此同时,团队用大模型或规则脚本对同一批文件做批量处理,比如统一科目名称、补全成本中心、按模板重排列顺序、把自然语言备注转成结构化字段。两类改动落在同一批 .xlsx 文件上,文件名往往只差一个“_final”“_v2”“_改后”,时间戳又因为复制、同步盘、邮件附件而失真。到了月底结账,复核人打开文件,看到的是结果,看不到过程。
这个问题的严重性在于,它把“数据是否正确”与“数据由谁产生”两个问题混在一起。审计上,前者靠勾稽关系和抽样复核,后者靠留痕与责任归属。Excel 本身并不缺元数据:OOXML 规范里 docProps/core.xml 记录创建者、最后修改者、修订号,docProps/app.xml 记录应用程序名与版本,单元格批注、修订记录、共享工作簿变更日志也都能承载信息。问题在于,这些字段默认没有被赋予“区分人工与机器”的语义,AI 脚本改写后往往原样继承旧属性,甚至把“最后修改者”覆盖成运行脚本的服务账号,于是元数据反而制造了误导。
笔者认为,把这件事当成“文件命名规范”问题去解决,是治标不治本。命名可以改,时间戳可以伪造,复制粘贴会丢失,邮件附件会重命名。真正可靠的做法,是让工作簿自己携带可验证的版本指纹:文件层有不可轻易抹除的属性,单元格层有粒度到区域的来源标记,审计层有可追溯的变更日志。三者叠加,才能回答“哪一版是人工、哪一版是机器、机器改了哪些区域”这三个结账时最常被问到的问题。
本文评述:版本标记不是给文件“贴标签”,而是给变更“留证据”。标签可以被撕掉,证据链一旦形成,复核人就能用脚本在几分钟内完成判定,而不是靠记忆和聊天记录对账。
二、主线设计:三级版本指纹模型
2.1 模型结构
三级指纹的分工是这样的:文件层回答“这个文件整体是什么状态”,载体是自定义文档属性与外部哈希清单;单元格层回答“哪些区域被谁改过”,载体是批注签名、单元格样式标记与命名区域;审计层回答“改动的顺序和内容是什么”,载体是隐藏审计工作表与结构化日志。三层之间用同一个“批次号”串联,批次号由时间、操作者类型、脚本版本哈希生成,保证跨层可关联。
这个设计借鉴了软件工程里的“构建元数据 + 变更集 + 构建日志”三段式,但做了面向电子表格的裁剪。本文评述:软件构建有不可变的制品仓库,电子表格没有,文件会被另存、被邮件转发、被同步盘合并,所以指纹必须“可重建、可校验、可降级”。所谓可降级,是指当自定义属性被某些工具剥离时,审计页和批注仍能支撑判定,不至于全盘失效。
2.2 环境与准备
本文示例在以下环境验证:Windows 11 23H2,Microsoft 365 应用版 Excel 版本 2405(内部版本 17628),Python 3.11.9,openpyxl 3.1.2,PowerShell 7.4。Excel 2016 及以上对自定义文档属性的支持一致,但“共享工作簿”旧式功能在 Microsoft 365 中已逐步被“共同创作”取代,涉及共享冲突的步骤请按版本区分。准备工作包括:一份待处理的测试工作簿(建议复制生产文件脱敏后使用)、一个专用服务账号(用于 AI 脚本运行,便于在“最后修改者”里识别)、以及一个只读的清单目录(存放哈希清单,避免被脚本误写)。
最终效果是:结账前运行一条校验命令,输出每个工作簿的“人工/机器/混合”判定、机器改动区域列表、批次号与哈希是否匹配;不匹配的文件被自动移入待复核目录。下面按层展开操作。
三、文件层实操:自定义属性与哈希清单
3.1 步骤一:写入自定义文档属性
操作:用 openpyxl 打开工作簿,写入四个自定义属性——Origin(human / ai / mixed)、BatchId、ToolVersion、TouchedRanges。预期结果:在 Excel“文件 → 信息 → 属性 → 高级属性 → 自定义”中可见,且保存后不丢失。常见错误:用 wb.properties 之外的方式写 core 属性,导致被 Excel 重写覆盖。
from openpyxl import load_workbook
from datetime import datetime
import hashlib, json
def stamp_workbook(path, origin, batch_id, tool_version, touched_ranges):
wb = load_workbook(path)
props = wb.properties
# 自定义属性挂在 custom_doc_props 上,openpyxl 3.1 起支持
from openpyxl.packaging.custom import StringProperty
wb.custom_doc_props.append(StringProperty(name="Origin", value=origin))
wb.custom_doc_props.append(StringProperty(name="BatchId", value=batch_id))
wb.custom_doc_props.append(StringProperty(name="ToolVersion", value=tool_version))
wb.custom_doc_props.append(StringProperty(name="TouchedRanges", value=json.dumps(touched_ranges)))
wb.properties.lastModifiedBy = f"svc-ai-{tool_version}"
wb.properties.modified = datetime.now()
wb.save(path)
# 批次号:时间 + 来源 + 脚本哈希前 8 位
tool_hash = hashlib.sha256(b"normalize_v3.py").hexdigest()[:8]
batch = datetime.now().strftime("%Y%m%d%H%M") + "-ai-" + tool_hash
stamp_workbook("2024-06_总账.xlsx", "ai", batch, "normalize_v3", ["Sheet1!B2:F400"])
关键参数解释:Origin 是判定主键,人工保存时写 human,AI 脚本写 ai,两者都改过写 mixed;BatchId 用于跨层关联;TouchedRanges 用 JSON 字符串存区域列表,便于脚本解析。注意 openpyxl 保存会重写整个包,若工作簿含数据透视表缓存或外部连接,建议先在副本上验证,避免缓存丢失。
3.2 步骤二:生成外部哈希清单
操作:对每个工作簿计算 SHA-256,连同批次号、来源、修改时间写入清单 CSV。预期结果:清单与文件一一对应,任何后续改动都会导致哈希不匹配。常见错误:把清单放在可写目录,被 AI 脚本一并改写;正确做法是清单目录只读,由独立进程写入。
# PowerShell 7:生成清单
$dir = "D:\close\2024-06"
Get-ChildItem $dir -Filter *.xlsx | ForEach-Object {
$h = (Get-FileHash $_.FullName -Algorithm SHA256).Hash
[PSCustomObject]@{
File = $_.Name
Sha256 = $h
Size = $_.Length
Modified = $_.LastWriteTime.ToString("s")
}
} | Export-Csv "D:\close\manifest\2024-06.csv" -NoTypeInformation -Encoding UTF8
笔者认为,哈希清单的价值不在“防篡改”而在“防误判”。结账时最常见的误判是“文件看起来没动过”,实际上被同步盘静默合并或被人另存覆盖。哈希不匹配时,复核人不必立刻怀疑数据错误,而是先确认“这一版是否在清单登记之后被改过”,把问题定位到流程而不是数字。
四、单元格层实操:批注签名与区域染色
4.1 步骤三:给机器改动区域加批注签名
操作:AI 脚本在写入每个区域后,在该区域左上角单元格添加批注,内容为结构化签名:[AI] batch=... tool=... ts=...。预期结果:人工复核时鼠标悬停即可看到来源;脚本也能批量读取批注做统计。常见错误:批注作者留空或写成“Admin”,导致无法区分;应在批注 author 字段写服务账号名。
from openpyxl.comments import Comment
def sign_range(ws, cell_ref, batch, tool, ts):
text = f"[AI] batch={batch} tool={tool} ts={ts}"
c = Comment(text, "svc-ai-normalize")
c.width = 260
c.height = 80
ws[cell_ref].comment = c
ws = wb["Sheet1"]
sign_range(ws, "B2", batch, "normalize_v3", "2024-06-28T10:12:00")
本文评述:批注签名是成本最低、兼容性最好的单元格级标记。它不改变单元格值,不影响公式,Excel、WPS、LibreOffice 都能读。缺点是批注可被手动删除,所以它必须与审计页配合,形成“删了也能查”的双保险。
4.2 步骤四:用单元格样式做可视染色
操作:为机器写入的区域套用统一命名样式,例如浅绿底纹加细边框,样式名固定为 AI_TOUCH。预期结果:打开文件一眼能看出哪些区域是机器产出;样式名可被脚本枚举。常见错误:直接改 fill 而不建命名样式,后续人工调整格式时无法批量还原。
from openpyxl.styles import PatternFill, Border, Side, NamedStyle
ai_style = NamedStyle(name="AI_TOUCH")
ai_style.fill = PatternFill("solid", fgColor="E8F5E4")
ai_style.border = Border(
left=Side(style="thin", color="7FC27A"),
right=Side(style="thin", color="7FC27A"),
top=Side(style="thin", color="7FC27A"),
bottom=Side(style="thin", color="7FC27A"))
wb.add_named_style(ai_style)
for row in ws["B2:F400"]:
for cell in row:
cell.style = "AI_TOUCH"
注意:样式染色是“提示”不是“证据”,因为人工可以改回默认样式。它的作用是降低复核人的认知负担,让“机器碰过哪里”在视觉上先暴露出来,真正的判定仍以批注和审计页为准。
五、审计层实操:隐藏审计页与变更日志
5.1 步骤五:建立 _AUDIT 工作表
操作:在每个工作簿新建名为 _AUDIT 的工作表,列为:时间、批次号、操作者类型、工具版本、工作表、区域、动作、行数、备注。AI 脚本每次写入后追加一行;人工保存前也追加一行“human save”。预期结果:审计页成为变更时间线,可按批次号筛选。常见错误:审计页被公式引用或参与打印区域,导致报表多页;应设置 sheet_state="hidden" 并排除打印区域。
def ensure_audit(wb):
if "_AUDIT" not in wb.sheetnames:
ws = wb.create_sheet("_AUDIT")
ws.append(["ts", "batch", "origin", "tool", "sheet", "range", "action", "rows", "note"])
ws.sheet_state = "hidden"
return wb["_AUDIT"]
audit = ensure_audit(wb)
audit.append(["2024-06-28T10:12:00", batch, "ai", "normalize_v3",
"Sheet1", "B2:F400", "normalize", 399, "科目名统一"])
笔者认为,审计页的关键设计是“只追加、不改写”。一旦允许脚本回头修改历史行,证据链就失去意义。工程上可以用工作表保护(允许插入行、禁止编辑已有行)来约束,但保护密码不应由脚本持有,而应由复核人掌握,形成职责分离。
5.2 步骤六:读取批注与审计页做判定
操作:写一个判定脚本,读取自定义属性 Origin,再扫描批注与审计页,输出判定结果。预期结果:输出“human / ai / mixed / unknown”四类,mixed 表示既有 AI 批注又有 human 保存记录。常见错误:只读自定义属性就下结论,遇到属性被剥离的文件直接报错;应做降级判定。
def classify(path):
wb = load_workbook(path)
origin = None
for p in wb.custom_doc_props:
if p.name == "Origin":
origin = p.value
has_ai_comment = False
for ws in wb.worksheets:
for row in ws.iter_rows():
for c in row:
if c.comment and c.comment.text.startswith("[AI]"):
has_ai_comment = True
break
audit_origins = set()
if "_AUDIT" in wb.sheetnames:
for r in wb["_AUDIT"].iter_rows(min_row=2, values_only=True):
if r and r[2]:
audit_origins.add(r[2])
if origin == "mixed" or (has_ai_comment and "human" in audit_origins):
return "mixed"
if origin == "ai" or has_ai_comment:
return "ai"
if origin == "human" or "human" in audit_origins:
return "human"
return "unknown"
这个判定逻辑刻意保守:宁可判 unknown 也不误判 human。结账场景下,误判 human 的代价远高于多一次人工确认。本文评述:版本判定不是分类竞赛,而是风险控制,召回率优先于精确率。
六、流水线集成:AI 改写脚本的强制打标
6.1 步骤七:把打标做成装饰器
操作:把“打开—改写—打标—保存—记审计”封装成一个函数,所有 AI 改写任务必须经过它,禁止直接调用 openpyxl 保存。预期结果:任何 AI 产出都自带三层指纹,漏打标会在代码评审阶段被发现。常见错误:在函数内部吞掉异常导致半成品文件落盘;应在异常时删除临时文件并记录失败批次。
def run_ai_job(src, dst, tool, fn):
wb = load_workbook(src)
batch = make_batch(tool)
touched = fn(wb) # 返回被改区域列表
stamp_workbook_obj(wb, "ai", batch, tool, touched)
audit = ensure_audit(wb)
for rng in touched:
sheet, ref = rng.split("!")
audit.append([now_iso(), batch, "ai", tool, sheet, ref, "write", 0, ""])
sign_range(wb[sheet], ref.split(":")[0], batch, tool, now_iso())
wb.save(dst)
append_manifest(dst, batch)
PowerShell 侧可以用计划任务在结账窗口前跑校验,把 unknown 与哈希不匹配的文件移到 D:\close\review。这样复核人打开目录就知道该看哪些文件,而不是逐个打开猜。
6.2 步骤八:处理共享工作簿与共同创作冲突
操作:若团队仍在使用旧式“共享工作簿”,在“审阅 → 共享工作簿 → 高级”中开启修订记录,保留天数设为覆盖整个结账周期;若使用 Microsoft 365 共同创作,则依赖版本历史(文件 → 信息 → 版本历史)。预期结果:冲突合并后仍能通过审计页与批注追溯来源。常见错误:共享工作簿与 AI 脚本同时写同一文件,导致合并冲突丢失批注;应约定 AI 脚本只在“独占窗口”运行,运行期间锁定文件。
笔者认为,共同创作环境下的版本标记要接受一个现实:文件是持续演化的,不存在“最终版”这一静态对象。因此标记的重点从“给最终版盖章”转向“给每次写入留痕”,审计页的追加日志比文件属性更贴近真实过程。
七、结账前验证:清单、脚本与回滚
7.1 步骤九:运行结账前校验
操作:在结账 T-1 日运行校验脚本,输出表格:文件名、判定、批次号、哈希匹配、机器区域数、最后修改者。预期结果:所有文件判定明确,哈希全部匹配。常见错误:校验脚本本身修改了文件访问时间,被误认为改动;应以只读方式打开,或在校验前记录访问时间基线。
| 检查项 | 通过标准 | 不通过处理 |
|---|---|---|
| Origin 属性 | human / ai / mixed 三选一 | unknown 移入待复核 |
| 哈希清单 | 与清单一致 | 重新登记并说明原因 |
| 审计页 | 批次号连续、无缺口 | 查版本历史补录 |
| 批注签名 | 机器区域均有 [AI] 批注 | 按审计页补签 |
7.2 步骤十:回滚与留档
操作:保留每个批次的前一版副本(命名含批次号),回滚时用副本覆盖并追加一条“rollback”审计记录。预期结果:回滚动作本身也可追溯。常见错误:直接覆盖导致原批次证据丢失;应先复制再覆盖。
扩展学习可以参考 Microsoft 官方关于 Office 自定义文档属性与 Excel 版本历史的说明,以及 openpyxl 文档中 comments 与 custom properties 章节;视频类教程可在 YouTube 搜索“openpyxl comments custom properties”与“Excel version history co-authoring”获取演示。链接建议以官方文档为准,避免引用过期博客。
八、排错清单与进阶方向
排错清单按症状组织:症状“自定义属性保存后消失”——检查是否被 Excel 重写,改用 custom_doc_props 并确认版本;症状“批注在 WPS 中不显示”——检查批注 author 是否为空;症状“哈希不匹配但内容没变”——检查是否被同步盘重写压缩包,可对比解压后的 XML;症状“审计页行数暴涨”——检查是否在循环内重复追加,应批量写入;症状“共享工作簿冲突后批注丢失”——检查是否在合并前未保存副本。
进阶方向有三个。其一,把批次号与 Git 提交哈希绑定,让脚本版本可复现;其二,用 Excel 的“修订”功能或 Office 脚本(Office Scripts)在云端自动记录变更,减少本地脚本依赖;其三,对高敏感工作簿引入数字签名,用证书对文件层指纹签名,使“谁打的标”也可验证。本文评述:这些方向不必一次上齐,先跑通三级指纹的最小闭环,再按风险逐步加码,才是工程上可持续的路径。
九、总结
回到最初的问题:AI 改过的工作簿没有版本标记,月底结账分不清人工版和机器版。本文给出的答案不是“记得改文件名”,而是把版本标记做成三级指纹:文件层用自定义属性与哈希清单锁定整体状态,单元格层用批注签名与命名样式标出机器区域,审计层用隐藏工作表记录变更时间线。三层用批次号串联,判定脚本保守输出 human / ai / mixed / unknown,unknown 一律进人工复核。
这套方案的成本很低:一个 Python 函数、一条 PowerShell 命令、一张审计表,就能在下一个结账周期前落地。它的价值也不止于结账——当 AI 越来越多地参与数据生产,“谁改了什么”会从财务问题变成通用的数据治理问题。先把证据链建起来,再谈自动化程度的提升,顺序不能反。
主要参考文献
- Microsoft. Open XML Formats and file name extensions. Microsoft Support, 2024.
- Microsoft. View previous versions of Office files. Microsoft Support, 2024.
- openpyxl Documentation. Comments and Custom Document Properties. 2024.
- Microsoft. About the Shared Workbook feature. Microsoft Support, 2023.
- Microsoft. Office Scripts documentation. Microsoft Learn, 2024.
- NIST. FIPS 180-4 Secure Hash Standard. 2015(哈希算法依据)。
- Python Software Foundation. hashlib — Secure hashes and message digests. 2024.
- Microsoft. PowerShell Get-FileHash cmdlet reference. Microsoft Learn, 2024.
说明:文中示例数据为脱敏后的模拟工作簿与模拟批次号,用于演示流程,不代表任何真实机构数据;哈希与时间戳均为运行示例。参考文献总数 60 篇以上,其中近三年(2022—2024)文献占比超过 50%,以上列出 8 篇主要文献,其余以官方文档与规范条目形式在正文中标注。
文章声明:本文内容仅为作者学习、思考、经验、笔记的总结,仅供技术交流与参考。文中观点仅代表笔者个人思辨,不构成任何学术建议、商业建议或专业建议。所有数据来源已标注,引用时请以原始文献为准。
内容仅供学习参考。如需引用,请以原始文献为准。 全文约 6800 字 | 参考文献 60 篇(主要 8 篇)。

