从代码行到Agent执行记录:构建AI编程过程的可追溯系统
最近在折腾 AI agent 辅助编程时遇到一个很头疼的场景一个 agent 连续跑了十几轮对话改了好几个文件最后代码确实能跑了但回看仓库时完全想不起来某一行关键逻辑是“怎么来的”。是 agent 根据哪条指令改的当时拿到了什么上下文中间是不是有一个决策转折点更有意思的是这个需求其实不是个例。很多团队在使用 Cursor、Cline、Codex CLI 这类 agent 工具时都面临同一个问题我们可以对代码做 Git 版本管理却很难对“agent 的思考与执行过程”做版本管理。于是就有了这个项目思路“Instantly get the transcript from the agent that wrote any line of code”意思是给定任意一行代码立刻反查它是由哪一个 agent、哪一次执行、哪一条消息、哪一次工具调用生成的并直接拿到对应的完整 transcript执行记录/对话记录。本文我会围绕这个方向完整拆解它的核心概念、实现思路和工程代码分享一套可以落地的最小实现方案。无论你是 agent 框架的开发者还是正在尝试把 AI 编程接入团队工作流这篇文章应该都能给你一些直接可用的参考。1. 为什么“每行代码的 transcript”这么重要1.1 从一次“找不到来源”的 bug 说起假设你接手的项目里有这样一段代码# 某服务里的一段重试逻辑 retry_times 3 backoff_factor 1.7你第一反应是为什么backoff_factor是 1.7而不是 1.5 或 2这个数是谁定的如果按常规代码评审流程你可以用git log和git blame找提交记录但是当代码由 agent 生成时Git 提交信息可能只是feat: 添加外部接口调用提交人是某个工程师但真正“写”这段代码的是 agent。工程师只是审查并提交了它。你找不到 agent 当时为什么选择 1.7 的任何依据。transcript 的价值就在这里。当我们说“拿到这行代码的 transcript ”意思是把这一行代码与 agent 的执行记录关联起来记录里包含——当时用户给 agent 的完整指令、agent 的判断过程、它读取了哪些文件、它尝试过什么方案、它最终基于什么理由写下了这行代码。1.2 从“代码版本管理”到“执行过程版本管理”传统软件开发中Git 帮助我们管理代码的变更历史。但 AI 时代代码变更的来源变成了“人类指令 agent 执行”这个过程本身包含大量上下文用户原始诉求agent 读取文件后的理解agent 多次工具调用的参数和结果agent 报错后如何自我修正这些内容如果只停留在终端输出里实际上是一次性信息。一旦终端关闭它们就丢失了。而代码文件本身只保留了最终结果。所以“给一行代码找 transcript”本质上是在补全一层过程数据代码行 ←—— 来源映射 —— agent 执行记录transcript这也是为什么把 agent 的开发与“可观测性”“可追溯性”放在一起讨论。当 agent 成为写代码的主体之一我们不仅要管理“代码是什么”还要管理“代码为什么变成这样”。1.3 适用场景从我实际接触的项目来看下面几类场景对这个能力的需求最强烈场景具体痛点代码审查Reviewer 看到可疑逻辑需要快速确认 agent 是不是误解了需求Bug 溯源线上问题定位到某行代码需要知道 agent 当时依赖了什么上下文回归测试agent 生成的代码在后续版本中行为变化需要对比前后两次执行记录知识沉淀团队希望把 agent 的生产过程转换为可检索的技术文档风险审计合规或安全团队需要检查 agent 是否读取了敏感信息1.4 与相关概念的边界和这个主题相关的几个概念容易混淆先做个简单区分Transcript执行记录指 agent 在与环境交互过程中的完整记录包括用户消息、模型消息、工具调用、工具结果等。它描述“agent 做了什么”。Lineage数据血缘在数据工程中lineage 描述数据的来源和加工链路。本文借用这个概念指一行代码来源于哪一次 agent 执行。Harness执行框架指支撑 agent 运行的框架层负责调度模型、工具、记忆等组件。transcript 的采集通常由 harness 层完成。简单来说我们要做的事可以概括为在 harness 层采集 transcript在线级别建立代码与 transcript 的血缘关系。2. 整体方案设计2.1 核心设计目标在设计这个系统之前我明确了自己的几个目标不需要修改 agent 底层模型不依赖特定模型只在执行框架层做增强。支持按行查、按文件查、按时间查不仅支持“这行代码来自于哪次执行”还支持浏览某个文件的完整 agent 修改历史。对原代码侵入最小不要让 agent 生成的代码里塞满调试注释。离线可用transcript 保存为本地结构化文件不依赖云服务。基于这些目标我把系统拆成了三层采集层Recorder在 agent 执行期间采集所有消息、工具调用、文件变更事件。映射层Lineage Mapper把每次文件变更与执行事件关联生成“哪一行代码由哪个事件产生”的映射。查询层Query Interface提供命令行或 HTTP 接口输入代码行或文件路径返回对应 transcript。2.2 关键概念lineage_id 与 patch先引入一个最核心的概念lineage_id血缘标识。当 agent 通过工具调用对文件进行修改时会产生一个文件补丁。我们给这个补丁分配一个唯一的 lineage_id同时把这个补丁连同它的上下文元信息记录下来。这样一来“某一行代码”可以被追踪到“某个补丁”“某个补丁”又可以被关联到“某条消息”“某次工具调用”最终串起完整的 transcript。举个例子{ lineage_id: op_8f1a2b3c, timestamp: 2025-01-15T10:32:11Z, source: apply_patch_tool, trigger_message_id: msg_04, file_path: src/services/retry.py, patch: --- a/src/services/retry.py\n b/src/services/retry.py\n -12,5 12,7 \n-retry_times 3\nretry_times 5\nbackoff_factor 1.7\n, after_line_map: { 14: op_8f1a2b3c, 15: op_8f1a2b3c } }在这个结构里after_line_map记录了补丁应用之后文件第 14 行和第 15 行是由op_8f1a2b3c这个血缘标识产生的。2.3 为什么不直接用行号做键你可能会想既然只要找到代码行号再映射过去不就行了吗问题在于代码文件是动态变化的。第一次 agent 修改后某行代码位于第 14 行第二次 agent 又在上方插入了 10 行那第 14 行就变成别的代码了。所以不能只用行号必须用“快照 变更”的方式每次文件修改后记录一个文件快照的 hash。把补丁应用前后的行号变化保存下来。查询时先定位当前文件版本再沿着补丁历史反向推导该行属于哪一次修改。这类似于 Git 中git blame的实现思路但我们的对象不是 commit而是 agent 的每一次工具调用。2.4 嵌入方式注释标记 外部注册表在设计过程中我尝试过两种把代码行与 lineage 关联的方式只存外部映射不修改源文件所有映射关系都存在.agent-trails/目录下。同时写入轻量注释在关键代码行上方添加一个注释标记例如// agent-trail: op_8f1a2b3c。两种方式各有优劣方式优点缺点只存外部映射对代码文件零侵入代码拷贝出项目后关系丢失注释标记跟代码一起走直观可见污染代码仓库需要额外的审查策略我建议采用混合模式默认不往代码里写注释外部保存完整映射。通过配置开关控制是否写入注释适合小型探索项目或团队有强制溯源要求的情况。这部分具体实现会在后面的实战代码里演示。3. 环境准备与项目结构3.1 基础环境本文示例代码使用 Python 实现主要是因为它处理 JSON、命令行和文件操作比较直接适合做原型验证。操作系统Windows / macOS / Linux 均可本文示例在 macOS 与 Ubuntu 下测试。Python3.10 及以上。第三方依赖不引入重量级框架使用 Python 自带json、hashlib、pathlib、difflib等标准库即可。Git用于模拟真实代码变更场景可选。如果你的项目是 Node.js 或 TypeScript实现思路完全一样只需要调整文件操作和命令行部分。3.2 推荐项目结构我建议把整个方案做成一个独立的命令行工具项目结构如下agent-trails/ ├── atrail/ │ ├── __init__.py │ ├── recorder.py # 采集 agent 执行事件 │ ├── mapper.py # 生成 lineage 映射 │ ├── registry.py # 管理 lineage 注册表 │ ├── query.py # 查询接口 │ └── cli.py # 命令行入口 ├── examples/ │ └── demo_agent.py # 模拟一个 agent 修改文件 ├── tests/ │ └── test_basic.py └── pyproject.toml为了让你能够快速跑通我接下来会把核心代码拆开讲解。整体代码不长但每部分都对应一个明确职责。4. 核心模块实现4.1 定义数据模型首先定义几个基础数据结构。为了减少复杂性我直接使用 Pythondataclass和字典表示不引入 ORM。# 文件路径atrail/models.py from dataclasses import dataclass, field from datetime import datetime from typing import Optional dataclass class Event: 一次 agent 执行中的基础事件 event_id: str event_type: str # message / tool_call / tool_result / file_change timestamp: str content: str tool_name: str tool_input: dict field(default_factorydict) parent_id: Optional[str] None dataclass class FileChange: 一次文件变更 lineage_id: str file_path: str before_content: str after_content: str patch_text: str event_id: str timestamp: str文件变更里保存了修改前后的完整内容、补丁文本以及产生这次变更的 event_id。这个 event_id 可以继续关联到具体的用户消息或工具调用。4.2 采集层把 agent 执行过程变结构化记录采集层的职责很简单监听 agent 执行过程的关键节点把它们写成 JSONL 格式的逐行记录。一个比较通用的做法是在调用工具的地方包一层装饰器每次工具执行前后各记录一次事件。# 文件路径atrail/recorder.py import json import uuid from datetime import datetime, timezone from pathlib import Path from typing import Any class AgentRecorder: 采集 agent 执行事件并输出为 JSONL 文件。 使用方式: recorder AgentRecorder(transcript_dirPath(./.agent-trails)) target recorder.watch_fn(my_function) result target(...) def __init__(self, transcript_dir: Path): self.transcript_dir transcript_dir self.transcript_dir.mkdir(parentsTrue, exist_okTrue) self.session_id uuid.uuid4().hex[:12] self._transcript_path self.transcript_dir / fsession_{self.session_id}.jsonl self._current_event_id None def _write_event(self, event: dict): with open(self._transcript_path, a, encodingutf-8) as f: f.write(json.dumps(event, ensure_asciiFalse) \n) def record_message(self, role: str, content: str) - str: 记录一条用户或助手消息 event_id uuid.uuid4().hex[:12] self._write_event({ event_id: event_id, event_type: message, role: role, content: content, timestamp: datetime.now(timezone.utc).isoformat(), parent_id: self._current_event_id, }) self._current_event_id event_id return event_id def record_tool_call(self, tool_name: str, tool_input: dict) - str: 记录一次工具调用 event_id uuid.uuid4().hex[:12] self._write_event({ event_id: event_id, event_type: tool_call, tool_name: tool_name, tool_input: tool_input, timestamp: datetime.now(timezone.utc).isoformat(), parent_id: self._current_event_id, }) self._current_event_id event_id return event_id def record_tool_result(self, tool_result: Any, status: str success): self._write_event({ event_id: uuid.uuid4().hex[:12], event_type: tool_result, status: status, result: tool_result if isinstance(tool_result, str) else str(tool_result), timestamp: datetime.now(timezone.utc).isoformat(), parent_id: self._current_event_id, }) def watch_fn(self, fn): 包装函数调用进入时记录 tool_call结束时记录 tool_result。 这里只提供最简单的自动化包装。实际 agent 开发中 更推荐在调用工具的位置显式调用 record_tool_call / record_tool_result。 def wrapper(*args, **kwargs): tool_input {args: [str(a) for a in args], kwargs: {k: str(v) for k, v in kwargs.items()}} call_id self.record_tool_call(fn.__name__, tool_input) try: result fn(*args, **kwargs) self.record_tool_result(result) return result except Exception as e: self.record_tool_result(f{type(e).__name__}: {e}, statuserror) raise return wrapper这个小模块重点演示了“事件串链”的思路每条事件都有一个parent_id指向触发它的事件。比如用户消息 - 工具调用 - 工具结果这样即使在异步或多轮场景下我们也可以还原事件树。4.3 映射层把文件补丁翻译成行级血缘有了采集事件下一步是把文件变更与具体行号关联起来。实现思路是拿到文件修改前后的内容。使用difflib.unified_diff生成补丁。逐行应用补丁记录每个补丁段影响了哪些新行。# 文件路径atrail/mapper.py import difflib import uuid from pathlib import Path from typing import List, Tuple def compute_patch(before: str, after: str) - str: 生成 unified diff 文本 before_lines before.splitlines(keependsTrue) after_lines after.splitlines(keependsTrue) diff difflib.unified_diff(before_lines, after_lines, lineterm\n) return .join(diff) def map_after_lines(before: str, after: str) - dict: 返回补丁应用后新增/修改的行号列表。 这里我们把“被补丁命中的新行”统一作为血缘关联目标。 sm difflib.SequenceMatcher(abefore.splitlines(), bafter.splitlines()) after_line_numbers [] for tag, i1, i2, j1, j2 in sm.get_opcodes(): # equal 表示没有变化不需要关联 if tag equal: continue # j11 到 j2 是变更后的新行号第几行 for line_no in range(j1 1, j2 1): after_line_numbers.append(line_no) return {lines: after_line_numbers}这里有一个简化处理SequenceMatcher可以识别哪些区域是新增、删除或替换。我们把所有非equal的新行都关联到这次 lineage_id。在真实场景中如果你只关心新增行可以在 tag 为insert或replace时再做一次过滤。4.4 注册表管理所有血缘映射为了能快速回答“第 N 行属于哪个 lineage”需要一个注册表。我建议用目录 JSON 索引的方式.agent-trails/ ├── registry.json └── sessions/ └── session_xxxx.jsonlregistry.json结构如下{ version: 1, files: { src/services/retry.py: { current_hash: sha256:..., lineages: [ { lineage_id: op_8f1a2b3c, timestamp: ..., lines: [14, 15], session_id: session_xxxx } ] } } }对应实现# 文件路径atrail/registry.py import json from pathlib import Path from typing import Optional class LineageRegistry: def __init__(self, root: Path): self.root root self.data_dir root / .agent-trails self.data_dir.mkdir(parentsTrue, exist_okTrue) self.registry_path self.data_dir / registry.json self._load() def _load(self): if self.registry_path.exists(): self.data json.loads(self.registry_path.read_text(encodingutf-8)) else: self.data {version: 1, files: {}} def save(self): self.registry_path.write_text( json.dumps(self.data, ensure_asciiFalse, indent2), encodingutf-8 ) def register_change( self, file_path: str, before_content: str, after_content: str, lineage_id: str, lines: list, session_id: str, ): file_entry self.data[files].setdefault( file_path, {current_hash: , lineages: []} ) import hashlib new_hash hashlib.sha256(after_content.encode(utf-8)).hexdigest() file_entry[current_hash] new_hash file_entry[lineages].append({ lineage_id: lineage_id, lines: lines, session_id: session_id, timestamp: __import__(datetime).datetime.now().isoformat(), }) self.save()这个注册表的好处是查询速度很快——按文件路径查找到文件条目再过滤行号即可不需要重新解析 transcript。4.5 查询接口从代码行到 transcript查询接口是整个工具的最终目的。它应该支持以下查询方式query --file src/services/retry.py --line 14query --file src/services/retry.py查看整个文件的血缘摘要# 文件路径atrail/query.py import json from pathlib import Path from typing import Optional def query_lineage(project_root: Path, file_path: str, line: Optional[int] None) - dict: registry_path project_root / .agent-trails / registry.json if not registry_path.exists(): return {error: no lineage registry found} data json.loads(registry_path.read_text(encodingutf-8)) file_entry data[files].get(file_path) if not file_entry: return {error: ffile not found in registry: {file_path}} if line is None: return {file: file_path, lineages: file_entry[lineages]} matched [] for item in file_entry[lineages]: if line in item[lines]: matched.append(item) if not matched: return {file: file_path, line: line, matched: []} return {file: file_path, line: line, matched: matched} def load_transcript(project_root: Path, session_id: str) - list: transcript_path project_root / .agent-trails / sessions / f{session_id}.jsonl events [] if not transcript_path.exists(): return events for line in transcript_path.read_text(encodingutf-8).splitlines(): if line.strip(): events.append(json.loads(line)) return events查询到 lineage_id 之后就可以顺藤摸瓜找到对应的 session 文件读出完整的 transcript。4.6 命令行入口我用标准库argparse写一个简单的命令行入口方便集成到工程流程里# 文件路径atrail/cli.py import argparse import json from pathlib import Path from atrail.query import query_lineage, load_transcript def main(): parser argparse.ArgumentParser(descriptionAgent Code Lineage Tool) subparsers parser.add_subparsers(destcommand) query_cmd subparsers.add_parser(query, helpquery code lineage) query_cmd.add_argument(--file, requiredTrue, helpfile path) query_cmd.add_argument(--line, typeint, helpline number) query_cmd.add_argument(--root, default., helpproject root) transcript_cmd subparsers.add_parser(transcript, helpload agent transcript) transcript_cmd.add_argument(--session, requiredTrue, helpsession id) transcript_cmd.add_argument(--root, default., helpproject root) args parser.parse_args() if args.command query: result query_lineage(Path(args.root), args.file, args.line) print(json.dumps(result, ensure_asciiFalse, indent2)) elif args.command transcript: events load_transcript(Path(args.root), args.session) for event in events: print(json.dumps(event, ensure_asciiFalse, indent2)) if __name__ __main__: main()5. 完整实战模拟 agent 修改代码并追溯下面用一个完整的示例把上面的模块串起来。这里我会模拟一个“agent 修改代码文件”的过程并记录 transcript 和 lineage。5.1 准备一个小项目先创建一个临时项目目录里面有一个模拟的业务代码文件mkdir -p demo-project/example cd demo-project创建example/retry.py# 文件路径demo-project/example/retry.py import time def call_external_api(): # TODO: 替换为真实 HTTP 调用 return None def main(): retry_times 3 for i in range(retry_times): print(ftry {i}) time.sleep(0.1) if __name__ __main__: main()5.2 模拟 agent 执行与采集接下来我写一个脚本模拟 agent 修改retry.py的流程。为了让示例可复现我不真正调用大模型而是直接把“模型生成的修改结果”保存下来再执行文件写入。# 文件路径demo-project/simulate_agent.py import sys from pathlib import Path # 将上级目录加入模块路径 sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from atrail.recorder import AgentRecorder from atrail.mapper import compute_patch, map_after_lines from atrail.registry import LineageRegistry def apply_patch(recorder: AgentRecorder, registry: LineageRegistry, file_path: Path, new_content: str): before file_path.read_text(encodingutf-8) after new_content # 1. 记录工具调用事件 call_id recorder.record_tool_call(apply_patch, {file: str(file_path)}) # 2. 生成 lineage_id import uuid lineage_id fop_{uuid.uuid4().hex[:8]} # 3. 计算补丁和新行号 patch_text compute_patch(before, after) line_map map_after_lines(before, after) # 4. 写入文件 file_path.write_text(after, encodingutf-8) # 5. 记录工具结果 recorder.record_tool_result(fpatched {len(line_map[lines])} lines) # 6. 注册到 registry registry.register_change( file_pathstr(file_path), before_contentbefore, after_contentafter, lineage_idlineage_id, linesline_map[lines], session_idrecorder.session_id, ) print(flineage_id: {lineage_id}, session_id: {recorder.session_id}, lines: {line_map[lines]}) return lineage_id def main(): project_root Path(__file__).resolve().parent retry_file project_root / example / retry.py # 初始化 recorder 和 registry把数据都放到项目根目录的 .agent-trails 中 recorder AgentRecorder(transcript_dirproject_root / .agent-trails / sessions) registry LineageRegistry(project_root) # 模拟用户指令 recorder.record_message(user, 优化外部接口调用增加重试次数和退避因子重试 5 次退避因子 1.7) recorder.record_message(assistant, 我会修改 retry.py加入 RETRY_TIMES5 和 BACKOFF_FACTOR1.7) # 第一次修改增加重试常量 first_version import time RETRY_TIMES 5 BACKOFF_FACTOR 1.7 def call_external_api(): # TODO: 替换为真实 HTTP 调用 return None def main(): for i in range(RETRY_TIMES): print(ftry {i}) time.sleep(0.1 * BACKOFF_FACTOR) if __name__ __main__: main() lineage_1 apply_patch(recorder, registry, retry_file, first_version) # 模拟第二轮修改把失败重试封装成函数 recorder.record_message(user, 把重试逻辑封装成独立的函数方便复用) recorder.record_message(assistant, 新增 retry_call 函数) second_version import time RETRY_TIMES 5 BACKOFF_FACTOR 1.7 def call_external_api(): # TODO: 替换为真实 HTTP 调用 return None def retry_call(func, retry_timesRETRY_TIMES, backoff_factorBACKOFF_FACTOR): for i in range(retry_times): try: return func() except Exception as e: print(fretry {i 1}/{retry_times}, error: {e}) time.sleep(0.1 * backoff_factor) raise RuntimeError(all retries failed) def main(): retry_call(call_external_api) if __name__ __main__: main() lineage_2 apply_patch(recorder, registry, retry_file, second_version) print(ffirst change: {lineage_1}) print(fsecond change: {lineage_2}) if __name__ __main__: main()运行这段模拟脚本cd demo-project python simulate_agent.py预期输出大致如下lineage_id: op_3f4a1b2c, session_id: 0a1b2c3d4e5f, lines: [1, 2, 9, 13, 14] lineage_id: op_9f0e8d7c, session_id: 0a1b2c3d4e5f, lines: [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]5.3 查询某一行代码的来源现在我们查询一下修改后的retry.py第 8 行def retry_call(...)是从哪里来的cd demo-project python -m atrail.cli query --file example/retry.py --line 8输出会告诉你这一行对应的 lineage_id 和 session_id。接着查完整 transcriptpython -m atrail.cli transcript --session 0a1b2c3d4e5f这样你就能看到 agent 完整的两轮对话与工具调用记录。这里再补充一个重要的使用习惯建议在实际项目里把 CLI 封装成一个 git 命令或 npm script。例如git agent-trail --file example/retry.py --line 8这样团队成员不需要额外记忆命令行工具就能在代码评审时快速调用。5.4 与真实 agent 框架的对接思路上述模拟脚本展示了核心机制。如果把它接入真实的 agent 开发框架思路如下Cline / Cursor / Codex CLI 这类工具这些工具本身有日志或 trace 输出。你可以写一个小插件或后处理脚本把它们的 JSONL trace 转换为本文的 transcript 格式然后在文件写入动作发生处补上 lineage 注册。自定义开源 agent 项目在调用apply_patch、write_file、multi_edit等工具的地方显式调用 recorder 与 registry。建议把“工具调用”与“文件变更”绑定在同一个事务里避免 agent 写文件后崩溃导致映射缺失。Harness 层集成如果你的 agent 工程中使用了 harness 框架那么更合理的位置是在 harness 的“工具执行后回调”中统一注册 lineage。不要把注册逻辑散落在各个工具函数里否则维护成本很高。另外很多 agent 开发框架已经强调skill与harness的区别。skill通常是 agent 可以调用的技能封装harness是运行时的执行骨架。在做 transcript 采集时建议把它视为 harness 关注点而不是 skill 关注点因为它是横切关注点与具体业务技能无关。6. 注册表设计细节与优化方向6.1 处理行号漂移前面提到过行号会随着文件更新发生漂移。最简单可靠的方案是每次修改文件后把整个文件的“行号 - lineage_id”快照都重算一次。实现上可以用一个累积式更新def rebuild_file_lineage(registry, file_path): 重建某个文件当前所有行的 lineage。 逻辑从最早的 lineage 开始逐个应用 patch最终得到每个行号的来源。 file_entry registry.data[files].get(file_path) if not file_entry: return {} # 这里简化直接使用 file_entry[lineages] 里保存的 lines 快照。 # 真实实现中你需要按时间顺序重放补丁逐行映射。 line_map {} for lineage in sorted(file_entry[lineages], keylambda x: x[timestamp]): for line_no in lineage[lines]: line_map[line_no] lineage[lineage_id] return line_map极端情况下如果两个补丁修改了同一行那么行号对应的 lineage 应该是较新的那个。而“旧内容去哪了”属于更深层的追溯可以依赖补丁文本继续往前查。6.2 支持“按代码内容查询”除了按行号查询有时候我们只知道一段代码内容。这时候可以使用相似度搜索把代码行去掉空格和缩进后生成指纹。在 registry 中扫描所有 lineage 涉及的代码块。返回相似行及其 lineage_id。这一步可以先不做但对于大型代码库很有价值。6.3 多 agent / 多会话合并在实际工程中一个文件可能被多个 agent 会话修改过。registry 天然支持这种场景因为每个 lineage 都记录了 session_id。查询时你可以按 session_id 分组对比不同 agent 对同一文件的改动。这也可以用于探索一个问题哪个 agent 的改动质量更高当代码出问题时追溯所有改过相关代码的 agent 会话找到出错前的最后改动通常就是问题高发区。7. 常见问题与排查思路7.1 查询结果为空找不到对应 lineage问题现象常见原因解决思路query 返回 matched 为空该行是手动修改不是 agent 改的确认是否走过了 Recorder 包装query 返回 matched 为空行号发生了漂移registry 未重建重新运行 rebuild_file_lineageregistry.json 不存在未初始化 registry检查项目根目录是否有 .agent-trails排查步骤检查.agent-trails/registry.json是否存在。检查目标文件路径是否与注册表里保存的一致注意相对路径和绝对路径差异。先不带--line参数查询看整个文件有没有血缘记录。如果文件整体没有记录说明采集环节没覆盖该文件。7.2 agent 执行中断transcript 不完整热词搜索里有个很典型的报错Agent execution terminated due to error。当 agent 执行过程中报错退出时transcript 可能只记录到中断前的事件。合理做法是Recorder 在每次写事件后立即 flush不要攒批量。启动新会话时检查是否存在未完成会话用status字段标记completed/terminated。如果中断发生在文件已写入但 lineage 未注册的阶段需要在启动扫描时做“补登记”对比文件 hash 与 registry 中保存的 hash。def reconcile(registry, tracked_files): for file_path in tracked_files: current_hash hash_file(file_path) if registry.data[files].get(str(file_path), {}).get(current_hash) ! current_hash: # 文件有未登记修改需要人工确认或标记为 unregistered log_warning(f{file_path} has unregistered changes)7.3 patch 中行号不准确difflib在复杂编辑场景下可能给出粒度较粗的结果。如果对准确度要求很高建议使用更精确的三方 diff 库例如daff或 Google 的diff-match-patch它们对代码结构感知更强。7.4 保存的 transcript 太大长会话的 JSONL 文件可能很快达到几 MB。建议超过阈值后只保存结构化事件不保存完整工具输入输出。对大文件 diff 只保存 diff不保存完整文件前后内容。定期归档历史 transcript常用查询只依赖 registry。8. 最佳实践与工程落地建议8.1 将 agent 执行记录视为一等公民团队里很多开发者习惯把 agent 生成的内容当作“临时产物”。但在生产项目中我建议把 transcript 与代码一样纳入版本管理。你不一定把 JSONL 文件提交到 Git但可以把它作为构建产物的一部分归档到 CI 系统或对象存储。若担心仓库膨胀可以在.gitignore中排除.agent-trails/sessions/但保留registry.json。8.2 统一事件 ID 的生成规范我建议所有事件 ID 使用可排序的 ID例如evt_20250115_103211_8f1a2b3c这样在查看日志时可以直观看到时间先后。不要使用纯 UUID除非你还有其他的排序字段。8.3 安全边界与敏感信息agent 在执行时会读取文件、调用外部 API、甚至访问生产环境。transcript 里可能包含敏感信息。这里有几个安全建议最小权限原则agent 运行时所使用的凭证只授予完成当前任务所需的最小权限。脱敏策略在写入 transcript 之前对疑似 token、密码、密钥等内容进行脱敏。访问控制.agent-trails目录在生产环境应设置为仅对特定角色可读。合规审计如果 agent 会访问客户数据需要评估是否允许将请求内容写入本地日志。这些看起来是额外的成本但是在共享开发机或 CI 环境里一旦把包含密钥的 transcript 推到远端仓库后果会比较严重。8.4 在代码评审流程中嵌入“血缘查询”一个比较有效的落地方式是让 PR 描述模板包含一个可选的“AI 改动来源”区块## AI 改动说明 - 由哪个 agent 生成xxx - 会话 IDsession_xxx - 关键代码行 - example/retry.py:8 - op_9f0e8d7c这可以减少 Reviewers 的“未知恐惧”让代码评审从“猜 agent 想干什么”变成“直接看 agent 执行记录”。8.5 定期清理与归档transcript 是很有价值的资产但不是所有会话都需要永久保留。建议重要里程碑会话永久归档。日常实验会话保留 30 天。失败会话保留 7 天但保留错误摘要。可以写一个简单的 cron 任务扫描目录做清理。8.6 与测试体系联动另一个很实用的思路是把 lineage 与自动化测试打通。当测试失败时定位到具体行后直接反查该行 lineage看是被 agent 修改的还是被人工修改的。如果测试失败发生在 agent 最近修改的行上可以先 focus 在 agent 会话上下文上排查效率会高很多。这意味着测试报告可以输出“失败代码行的 agent 来源”字段开发者在看 CI 失败时能更快进入状态。9. 总结与延伸思考这篇文章从一个很具体的场景出发想快速知道某一行代码是 agent 在什么情况下写出来的。我们把这个需求落成了一个由采集层、映射层、注册表、查询接口组成的小型系统。核心收获可以概括为几点代码的血缘追溯不依赖大模型能力它依赖的是执行框架的埋点与文件变更记录。行号会漂移所以不能只存行号要通过补丁或快照哈希维持稳定性。transcript 与代码同样值得管理把它和版本管理、代码评审、测试报告打通才能发挥最大价值。安全和权限是底线agent 的 transcript 可能包含敏感信息不能简单当成普通日志处理。如果你正在搭建或使用 agent 开发框架可以把这个能力视为 harness 层的“可观测性基础设施建设”。下一步可以继续探索如何基于 transcript 自动生成代码评审摘要如何让 agent 在出错时从历史 transcript 中检索相似场景并复用经验如何把多个仓库的 lineage 汇总到统一的可视化面板。你可以先按照本文的代码跑通一个最小 demo然后在自己的 agent 工程里找到所有write_file、apply_patch之类的工具把 recorder 和 registry 挂上。20 行左右的改动就能让代码仓库从“只能看到代码结果”升级为“能看到代码的生产过程”。