我们仓库里当时一共躺了九千多个源文件。任务听着也简单:让 AI 帮我把这几万个文件的模块关系梳理清楚,顺带给出重构建议。一开始我的想法很粗暴,把整个目录拖进上下文,让模型自己看。结果连试三轮都翻车,不是文件解析到一半挂了,就是模型开始分析 node_modules 里的压缩代码,最离谱的是它把一份 SQL 备份当成了业务配置文件,郑重其事写了一整页“配置说明”。
后来我停下来想明白一件事:把近万个源文件喂给 AI 之前,真正该做的不是“投喂”,而是先给这批文件做一次系统性的预处理和建档。这个动作谁都能做,做不做得到位,直接决定后面 AI 是帮你干活的工程师,还是只会复制粘贴的聊天机器人。这篇文章就把我当时完整跑通的流程、脚本和踩过的坑拆开讲清楚,适合所有准备把手头大规模代码/文档库交给 AI 分析、做 Agent 问答、或者整理数据喂给大模型的人。
1. 为什么“全量塞给 AI”这个思路注定会失败
1.1 上下文窗口和 token 成本,先从数学上就过不去
先别谈模型能力,我们先算一笔账。我当时那个项目目录 9,341 个文件,去掉明显无关的图片和二进制文件,剩下文本类源文件大概也还有七千多个。假设每个源码文件平均 260 行、每行 40 个字符,那就是大约 240 万行、9600 万字符。按常见 token 估算规则,英文和代码大概 4 个字符一个 token,这份语料总量得有两千多万 token。
现在主流商用模型上下文窗口做到 128k 或者 200k 已经算大了,你拿这些数字一除就明白,全量内容连 1% 都放不进去。就算你强行用支持超长上下文的方案,把所有文件压缩塞进去,输入费用也会瞬间膨胀到没法看。这还没提一个更隐性的问题:上下文中无关信息太多时,模型对关键文件的注意力会被稀释,回答质量肉眼可见地下降。
所以不要跟上下文窗口硬刚。正确的做法是理解一个现实:模型不需要一次读完所有文件,它只需要知道“这个仓库里有什么、每个文件大概负责什么”,然后按需去读真正相关的部分。这就是预处理要解决的第一件事:让 AI 手里先有一张准确的地图。
1.2 垃圾文件、构建产物和测试数据会带偏判断
很多人以为喂给 AI 的是“源文件”,实际目录里什么都有。我当时随手统计了一下,有编译出来的 .class、.pyc,有前端构建出来的 dist 目录,有将近 2GB 的 .git 历史,还有一堆 .DS_Store、Thumbs.db、日志文件、数据库导出 SQL,甚至还有从旧机器上拷过来的整个 .idea 配置目录。
这些文件对真正的源码分析没有贡献,反而会制造大量噪声。模型不具备你脑子里的“这个目录不用管”的常识,它只会老老实实把看到的所有内容都当作有效信息处理。于是它可能花大段文字分析一段压缩混淆后的 JS bundle,或者把测试夹具数据当成业务逻辑去推导,最后给你的结论自然跑偏。
更隐蔽的是重复文件。同一个文件在多个备份目录里出现好几份,或者旧版本、新版本、带后缀版本混在一起。模型一旦读到了过期版本,就会拿旧逻辑回答新问题。这些问题都不是靠模型多聪明能解决的,必须在投喂之前靠预处理把语料清洗干净。
1.3 来源复杂的文件,可能带病进门
“源文件”这几个字容易让人放松警惕,好像只要是文本就是安全的。但你如果接过那种从网盘、邮件附件、同事 U 盘、甚至数据库恢复工具里扒出来的目录,就知道里面有多乱。有些文件扩展名是 .txt,实际是个二进制文件;有些文件带着 Windows 系统上的“来自互联网”标记,本地解析器看一眼就拒绝打开;还有些文件编码混乱,GBK、UTF-8、UTF-16 混在一起,直接读出来就是乱码。
我特别提醒一点:直接从网上下载的源码包、书源文件或者别人分享的补丁目录,在喂给 AI 之前最好先做一次来源和内容体检。系统弹窗提示“该文件来自外部来源,可能不安全”不是玄学,它背后确实有一套安全机制在拦截可疑文件。你需要做的是甄别而不是无视:确认文件来源可信、杀毒扫描通过、没有夹带私货,再放进待处理队列。
如果这一步不做,轻则 AI 读到乱码开始胡编,重则把你不希望泄露的信息(比如藏在配置文件里的密码、密钥、内网地址)一起发给云端模型。这是工程问题,也是数据安全问题,不能马虎。
1.4 预处理的目标:制造“有效语料子集 + 按需访问方案”
我把整个思路理顺之后,给自己定了三个预处理目标。第一个目标,产出一份完整但精简的仓库索引,内容包括目录结构、语言分布、每个文件的路径、大小和行数。第二个目标,产出一份经过清洗的语料池,只保留文本类源文件和可解析的文档,统一编码、去除 BOM、修正换行符,必要时过滤超大文件和重复文件。第三个目标,设计一套按需读取机制,让 AI 通过工具函数主动读取某个具体文件的内容,而不是把几万个文件一次性堆给它。
简单说,就是先给 AI 地图,再让它自己决定去哪栋楼、找哪个人。这套思路不仅适用于代码分析,也适用于任何大规模文档投喂场景,本质上是把“把数据库搬进对话”改成“在对话里装一个检索器”。预处理做完之后,我自己实测的效果非常明显,AI 不再满嘴跑火车,开始能答到点子上。
2. 我先做的这件事:给整个源文件库做“体检与建档”
2.1 第一步先盘清家底,别凭感觉估数量
动手预处理的第一步,不是写清洗脚本,而是先把仓库里到底有什么东西彻底摸清楚。我当时很多人会直接说“大概有一万多个文件吧”,这种模糊认知就是后续所有问题的根源。你必须先回答几个精确的问题:文件总数是多少?总大小多大?哪些目录占了大部分空间?扩展名分布什么样?哪些文件特别大?
我的处理方法很简单,直接用一个 Python 脚本全局扫描一遍目录,输出几个统计表。我不建议在这种摸底阶段就上特别复杂的工具,先跑一段几十行的脚本拿到准确数字,比任何 fancy 的工具都管用。
from pathlib import Path from collections import Counter import os ROOT = Path("./repo") total_files = 0 total_size = 0 ext_counter = Counter() dir_counter = Counter() large_files = [] for p in ROOT.rglob("*"): if not p.is_file(): continue # 主动跳过隐藏目录和版本控制目录 if any(part.startswith(".") for part in p.relative_to(ROOT).parts): continue total_files += 1 size = p.stat().st_size total_size += size ext = p.suffix.lower() if p.suffix else "[noext]" ext_counter[ext] += 1 top_dir = p.relative_to(ROOT).parts[0] if p.relative_to(ROOT).parts else "?" dir_counter[top_dir] += 1 if size > 5 * 1024 * 1024: large_files.append((size, str(p))) print(f"文件总数: {total_files}") print(f"总大小: {total_size / 1024 / 1024:.1f} MB") print("扩展名分布 Top 20:") for ext, cnt in ext_counter.most_common(20): print(f" {ext or '[noext]'}: {cnt}") print("顶层目录分布 Top 10:") for d, cnt in dir_counter.most_common(10): print(f" {d}: {cnt}")这个脚本跑一遍,我对仓库就有了非常具体的认知。比如我那次就发现,扩展名排第一的居然是 .json,数量超过两千个,里面有配置文件、测试夹具、翻译文件,还有一堆不知道哪来的第三方数据;.py 文件只有一千多个,并没有我预想中那么多。顶层目录里光 test_data 就占了三千多个文件。这些数字直接影响了我后面的筛选策略。
在执行这一步时有个小提醒:用Path.rglob("*")会遍历所有子目录,当仓库里有 node_modules、dist、.git 这类巨型目录时,脚本会把它们也统计进去。我在上面的代码里通过跳过以点开头的目录来规避 .git,但 node_modules 没有点前缀,如果你不想统计它,最好先手工把要排除的目录名写成一个大集合,在遍历时直接跳过。
2.2 用 magic bytes 识别真实类型,别轻信扩展名
盘完家底之后,第二步是给文件做“真实身份”鉴别。扩展名太容易骗人了。我见过很多文件,文件名结尾是 .txt,用文本编辑器打开却是乱码,因为它本来就是某个程序写出来的日志文件或者半二进制格式。真正可靠的做法是读取文件头部的 magic bytes,也就是文件开头的几个字节,判断真实格式。
在命令行里,file命令就是干这个的:
file suspicious.dat file --mime-type -b unknown.xyz这个命令几乎所有的 Linux 和 macOS 都自带,Windows 下也可以用 Git Bash 或者 WSL 调用。它能在不看扩展名的情况下,告诉你一个文件到底是 ASCII 文本、UTF-8 文本、PNG 图片、PDF 文档、SQLite 数据库还是某个程序的可执行文件。
我用这个命令把仓库里“扩展名和真实类型不一致”的文件都筛了出来,数量相当惊人。很多没有扩展名的文件其实是 SQLite 数据库,很多 .txt 其实是老的 GB2312 编码文本,还有几个 .dat 文件居然是 ZIP 压缩包。这些文件如果不管三七二十一全喂给 AI,模型要么读到乱码,要么把二进制内容当成指令去理解,很可能产生幻觉。
落到代码层面,我建议直接读取文件开头一小段字节做判断。比如文本文件通常可以尝试用 UTF-8 解码,二进制文件会在解码时直接抛异常。下面的思路可以作为筛选代码:
def looks_like_text(path, sample_size=4096): try: with open(path, "rb") as f: raw = f.read(sample_size) # 空文件不算有效源文件 if not raw: return False # 尝试 UTF-8 解码(允许 BOM) raw.decode("utf-8") return True except UnicodeDecodeError: return False但是这里有个坑:纯文本文件也可能是 GBK/GB2312 编码的,UTF-8 解码会失败,但这种文件依然是“人类可读的源文件”,不能直接当成二进制排除。所以更稳妥的方案是“先尝试 UTF-8,失败后再用编码探测库判断,如果仍然不是常见编码,再归类为二进制”。
2.3 外部来源文件先检查“来源标记”和敏感信息
这一步是我后来才补上的,但是我认为经验价值很高。很多源文件不是你自己写的,而是从网上下载、同事发来或者从压缩包里解压出来的。这些文件在 Windows 和 macOS 上可能会被系统打上一个叫做 Mark of the Web(MOTW)的标记,表示它来自互联网。你在本地用某些解析工具打开时会遇到拦截,KKFileView 这类预览服务经常会提示“文件来源不受信任,拒绝访问”,其实是这个标记在起作用。
处理方式不是绕过安全机制,而是先确认来源可信、再解除标记。Windows 下可以用 PowerShell 批量清除下载文件的标记:
Get-ChildItem -Path . -Recurse | Unblock-FilemacOS 下则可以用 xattr 查看并删除隔离属性:
xattr -l yourfile xattr -d com.apple.quarantine yourfile删除标记之后,本地预览服务、解析器就能正常访问这些文件了。但我要强调,解除标记不等于文件安全。如果这批文件是别人整理好发给你的,你最好先跑一遍本地杀毒扫描,再用脚本查一下里面是否包含密钥、密码、内网 IP、手机号之类的敏感信息。我就是这么发现仓库里有一个历史遗留的 config 文件,里面写着一组数据库明文密码,幸好提前筛出来了。
2.4 统一编码和换行符,文本文件要“洗”成标准形态
编码问题听起来很基础,但遇到老项目或者跨平台来源的文件时,绝对能恶心到你。我那个仓库里,一部分文件是 UTF-8,一部分是 GBK,还有几个是 UTF-16 编码的配置文件。直接在 Python 里用默认编码读,非 UTF-8 的文件秒秒钟抛异常;就算勉强读出来,中文字符在模型眼里也是一堆乱码“锟斤拷”,它根本没法理解。
我后来在预处理流程里加了一个“文本标准化”环节,大致逻辑是:先尝试 UTF-8 解码,失败后用 chardet 这类库去猜编码,猜出来之后按对应编码解码,最后统一转成 UTF-8 并去掉 BOM。换行符也统一转成\n,避免 Windows 的 CRLF 混在里面引发不必要的解析问题。下面这段代码是我清洗阶段核心逻辑的简化版:
import chardet def normalize_text(raw: bytes) -> str | None: # 去掉 UTF-8 BOM if raw.startswith(b"\xef\xbb\xbf"): raw = raw[3:] return raw.decode("utf-8", errors="strict").replace("\r\n", "\n") try: return raw.decode("utf-8").replace("\r\n", "\n") except UnicodeDecodeError: pass # 用 chardet 猜编码,只作为 fallback guess = chardet.detect(raw[:4096]) enc = guess.get("encoding") if not enc: return None try: return raw.decode(enc).replace("\r\n", "\n") except (UnicodeDecodeError, LookupError): return None这里有一个很关键的细节:chardet 的结果只能当 fallback,不能当首选。它的探测准确率在短文件和纯 ASCII 文件上并不稳定,如果一上来就盲信 chardet,很可能把一个本来好好的文件转错成乱码。我在实际踩坑中得出的经验是:能用 UTF-8 解的绝不猜,猜的时候只取前几 KB 做采样,解码失败就放弃而不是强制替换。宁可少一个文件,也不要让一个乱码文件混进语料里污染后续回答。
清洗完的文本不要直接修改原始文件,建议统一输出到一个独立的 staging 目录,保持源目录的只读状态。这样你随时能回溯对比“清洗前”和“清洗后”的差异,也方便在发现问题时重新跑流程。
3. 从零复现:一套完整的“扫描-清洗-索引-投喂”流程
3.1 五类文件先分流:源码、文档、数据、备份、垃圾
预处理不能一刀切,第一步应该做“分流”。我后来每次处理目录,都会把文件分成五类。第一类是真正的源代码文件,比如 .py、.java、.js、.ts、.go、.c、.cpp、.h 这些,这是投喂的重点。第二类是可读文本文档,比如 Markdown、纯文本、HTML、YAML、JSON 配置文件,AI 可以读,但优先级低于源码。第三类是二进制文档,比如 PDF、Word、Excel,模型不能直接读,需要先转成纯文本再用。第四类是恢复数据/备份文件,比如 SQL 导出、.ibd/.frm 这类 MySQL 数据文件、.bak、.zip 压缩包,一般和源码分析无关,默认排除。第五类是真正的垃圾文件,包括临时文件、缓存文件、系统文件、构建产物,直接过滤。
这个分流逻辑必须写死在脚本里,不然每次手工挑文件会累死人。我当时用的核心过滤规则大致是这样:
EXCLUDE_DIRS = { ".git", "node_modules", "dist", "build", "__pycache__", ".idea", ".vscode", "target", ".gradle", ".cache" } EXCLUDE_EXTS = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".webp", ".class", ".jar", ".war", ".pyc", ".pyo", ".zip", ".tar", ".gz", ".bz2", ".7z", ".rar", ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".woff", ".ttf", ".eot", ".mp4", ".mp3", ".avi", ".exe", ".dll", ".so", ".dylib", ".o", ".a", ".ibd", ".frm", ".myd", ".myi", ".bak", ".sql" } MAX_FILE_BYTES = 2 * 1024 * 1024 # 超过 2MB 的文本文件单独挑出来看排除 .sql 和 .ibd 这一条,是我在处理一个混入数据库恢复文件的目录时总结出来的。很多人从 MySQL 数据恢复工具里扒出来一堆 .ibd、.frm 文件,以为这也算源文件,想一起喂给 AI。这些文件是 InnoDB 表空间和表结构定义文件,本质是二进制格式,喂给 AI 除了把上下文灌爆之外没有任何价值。真要做数据库恢复,那是另一个专业话题,别和代码分析混在一起。
3.2 扫描、去重、清洗的主脚本,照着改就能用
把 3.1 的过滤规则和上一节的编码标准化合并起来,就形成了一个比较完整的预处理脚本。流程是:遍历目录,跳过排除目录,过滤扩展名,按大小排除明显不该进语料的超大文件,对文本文件做编码归一化,同时计算行数和大小。最终生成一个 files.json,里面记录每一个有效文件的路径、大小、行数、语言分类。
我再补充一个去重逻辑。如果目录里存在大量内容完全一致的文件,AI 会读到同样的信息好几遍。比较文件内容是否一致的快速方法,是先按文件大小分组,只有大小相同的文件才进一步计算哈希。
import hashlib from pathlib import Path def file_hash(path: Path, chunk_size=65536): h = hashlib.md5() with open(path, "rb") as f: while chunk := f.read(chunk_size): h.update(chunk) return h.hexdigest() def find_duplicates(file_paths): by_size = {} for p in file_paths: size = p.stat().st_size by_size.setdefault(size, []).append(p) dup_groups = [] for size, paths in by_size.items(): if len(paths) < 2: continue hashes = {} for p in paths: h = file_hash(p) hashes.setdefault(h, []).append(p) for group in hashes.values(): if len(group) > 1: dup_groups.append(group) return dup_groups清洗输出到 staging 目录的操作,我建议保留原文件路径结构,不要把所有文件平铺到一个目录。否则后续 AI 按路径读取时,看不到包名和模块路径,就会丢失目录结构里蕴含的架构信息。比如 Java 的包名路径、Python 的包层级,这些对模型理解项目结构很重要。
清洗过程中导出的每一条记录,最好都附带一行“来源路径”,这样后续如果 AI 引用了某个文件里的内容,你能反查到底来自哪个文件,方便核对。
3.3 索引文件不要做成“第二份全量文本”,要做成地图和目录
很多人有个误区,觉得建索引就是把文件内容摘要一下然后全部塞进去。这也不行,摘要也是文本,几百上千个文件摘要加起来依然会爆上下文。我实际使用下来,比较有效的做法是生成两级信息。
第一级是给 AI 的首屏“仓库地图”,控制在几百个 token 以内。它只讲整体结构,比如有哪些顶级模块、每个模块大概多少文件、技术上是什么语言、明显的分层是 MVC 还是其他结构。第二级是一份更完整的 files.json,所有文件的路径、大小、行数都在里面,但这个文件不是一次性喂给模型,而是“按需查”的资源。
首屏地图我一般用 Markdown 手写或者脚本生成,大致长这样:
# REPO_MAP ## 顶层模块 - backend/: Python FastAPI 服务,约 1200 个源文件 - app/api/: 路由与接口层 - app/services/: 业务逻辑层 - app/models/: ORM 模型层 - frontend/: React + TypeScript,约 800 个源文件 - src/pages/: 页面组件 - src/components/: 通用组件 - scripts/: 运维脚本和数据处理脚本,约 300 个 - tests/: 测试目录,约 1800 个文件,与主代码 1:1 对应模型看到这个地图,基本上就明白这个项目哪里有东西。它如果想知道某个服务具体实现,再根据问题去查询 files.json,定位到具体路径后,用 read_file 工具读取那一个文件的内容。整个交互模式从“我塞给你所有文件”变成“你先看目录,再看你需要的文件”,上下文占用直接降了几个数量级。
3.4 让 AI 按需读文件,工具函数怎么写
我的做法是给模型暴露一个极简的读文件工具。如果你用的是 Agent 类框架,可以直接注册一个函数给模型调用;如果只是用 API 对话,就用 function calling 功能把工具描述传过去。工具逻辑非常简单:接收一个相对路径和可选的行范围,返回目标文件里的内容,并且自动加行号。
def read_file(relative_path: str, start_line: int = 1, end_line: int = None) -> str: base = Path("./staging_repo") # 防目录穿越 safe_path = (base / relative_path).resolve() if not str(safe_path).startswith(str(base.resolve())): return "ERROR: invalid path" if not safe_path.exists() or not safe_path.is_file(): return "ERROR: file not found" lines = safe_path.read_text(encoding="utf-8", errors="replace").splitlines() total = len(lines) if end_line is None: end_line = total end_line = min(end_line, total) selected = lines[start_line - 1:end_line] numbered = [f"{i + start_line}\t{line}" for i, line in enumerate(selected)] return "\n".join(numbered)注意这里有个安全细节:一定不要直接拼接用户或者模型传来的路径,不然容易发生目录穿越。模型本身不是恶意的,但它可能在推理中拼出带../的路径,导致读到 staging 目录之外的文件。用 resolve 之后做前缀校验,虽然多写几行代码,但能避免诡异问题。
给模型的 prompt 里,我会明确写清楚:
你需要分析代码时,不要一次性读取整个仓库。请先查看 REPO_MAP.md 了解项目结构。如果要定位具体实现,使用 read_file 工具读取目标文件。优先从入口文件和路由文件入手。每次最多读取 1000 行,根据需要分多次读取。这种限制看起来有点啰嗦,但实测能显著减少模型“试图一口气读完全部文件”导致的上下文浪费。
3.5 可疑文件先预览再放行,KKFileView 这类工具能帮上忙
有些文件通过了扩展名检查,也通过了文本检测,但我仍然不放心,比如从网上下载的文档、同事发来的压缩包里的文件。这时候我会用 KKFileView 这类开源的在线预览服务在本地先打开看一眼,确认内容确实正常再放行进语料池。
KKFileView 的部署很简单,在已经装好 Docker 的机器上一条命令就能启动:
docker run -d -p 8012:8012 keking/kkfileview启动后浏览器访问http://localhost:8012,上传文件就能在线预览很多格式,不需要在本地装 Office 全家桶。这个工具本身不是给 AI 用的,它的价值在于让你能快速抽样检查预处理结果里的可疑文件,而不是把几百个文件挨个用本地软件打开浪费一下午。
如果你处理的目录里没有可疑文件,这一步可以跳过。但每次从外部拿到的代码包和数据包,我都会抽 5% 到 10% 的文件人工快速浏览,确认没有夹带私货。这个习惯帮我躲掉过至少三次被塞进来历不明脚本的坑。
4. 预处理过程中的高频问题和排查技巧实录
4.1 问题:test 文件数量太多,把主代码淹没了
普通开发者写测试当然是好事,但喂给 AI 分析业务逻辑时,几千个测试文件的噪声会干扰模型判断。我那个仓库里有接近一半文件带 test 字样,模型看到这么多文件会以为测试逻辑就是主业务,回答里经常出现莫名其妙的断言和 mock 建议。
我在索引里专门划了一个“test: true”字段,并且把测试目录和主代码目录分开展示。如果分析任务只关心业务实现,那我会在 prompt 里直接加一句“忽略 tests 目录,除非问题明确涉及测试”。如果你处理的任务和测试相关,再单独把测试文件拿出来喂给 AI,效果会好很多。
4.2 问题:文件明明存在,AI 却说什么都找不到
这里通常不是模型能力问题,而是我给它的地图没有更新到最新状态。清洗之后我把文件复制到了新的 staging 目录,但 REPO_MAP 里某些路径还是用的源目录相对路径,导致模型按照地图去读文件,结果跑到了不存在的路径上。
解决方式很朴素,在生成索引和地图之后加一个自动校验环节,把所有索引里记录的路径都检查一遍是否存在。如果不存在就标记删除或者修正路径。这一步脚本半小时就能好,但能省掉后续跟模型反复拉扯确认文件的巨大麻烦。
4.3 问题:编码统一后,还是有个别文件变成乱码
我在第 2 节说过 chardet 不能作为首选,就是因为这个。有个文件原本是 GB2312 编码,我第一版脚本先用 UTF-8 解码失败后,chardet 居然把它识别成了 ISO-8859-1,结果转换出来全是乱码。这种错误不会报异常,因为 ISO-8859-1 可以解码任何字节序列,但它会静默地产生错误内容。
修复思路是给文本检测加一个“中文编码偏好”逻辑。如果文件里包含非 ASCII 字符,且原始编码是 ISO-8859-1 或 Windows-1252,而文件明显有中文特征,就要强制尝试 GBK/GB2312/Big5 等编码。另外还要对转换结果做一次质量校验,看是否包含大量替换字符\ufffd或者极其常见的乱码模式,有的话就打回重新走编码探测流程。
4.4 问题:大文件混进来,索引没爆,模型先卡了
仓库里常常有一个几十 MB 的日志文件、JSON 导出文件或者 SQL dump。这些文件虽然扩展名合理,也能当作文本处理,但你要真让模型去读它,上下文分分钟被撑爆。我当时索引里最大的文本文件有 40 多 MB,接近一千万字符,足够把常见模型的窗口全部灌满。
处理方式是对超大文本文件单独设置阈值。超过 2MB 的文件不直接进入可投喂列表,而是单独生成一个“超大文件清单”,里面记录路径、大小、前 100 行的预览内容。这样模型起码知道它存在,如果问题确实跟这个文件相关,再决定是否要按片段读取,而不是让这个庞然大物从一开始就待在上下文里。
4.5 问题:模型在找不到答案时开始编造文件路径
模型有一个很讨厌的倾向,就是它会“脑补”不存在的文件路径。比如我问某个功能怎么实现,如果索引里没有直接对应的文件,它会编一个src/utils/helper.py这样的路径出来,然后一本正经地解释里面的代码。这种事在缺少文件级工具约束时特别容易发生。
我的对策是双重的。一方面,在 prompt 里明确要求“所有引用的文件路径必须来自 files.json,不存在就不要引用,可以回答找不到”。另一方面,在工具层做路径校验,任何一个读文件请求如果指向了不存在的路径,就返回明确的错误信息,而不是抛一个让模型自行猜测的模糊异常。经过这两层约束,编造路径的问题基本被压住了。
4.6 常见问题速查表
下面这张表是我自己后来做同类任务时经常翻的排查清单,列几个最高频的症状和处理方案,省得每次重新踩坑。
| 症状 | 常见原因 | 处理建议 |
|---|---|---|
| 模型读文件时出现大量乱码 | 编码未统一,GBK/UTF-16 混入 | 预处理阶段加编码归一化,统一转 UTF-8 |
| AI 分析时老提 node_modules/构建产物 | 排除规则没生效 | 检查 EXCLUDE_DIRS,确认脚本真的跳过目标目录 |
| 回答内容前后矛盾像看了两个版本 | 仓库里有重复文件或旧版本备份 | 用哈希做去重,只保留一份 |
| 同一个文件被重复读取,上下文很快耗尽 | 没有按需读取机制 | 改用 read_file 工具,按行范围按需加载 |
| 模型引用的文件路径根本不存在 | 模型幻觉,路径没有校验 | 索引路径校验 + 工具层禁止读不存在文件 |
| 某些文档文件本地预览正常,工具里打不开 | 文件带外部来源标记 | 先清理 MOTW 属性,再让解析器访问 |
| 几千个测试文件干扰主业务分析 | test 文件没有单独隔离 | 索引中标记测试文件,prompt 里按需排除 |
这些问题的共性是:它们没有一个是靠“换个更聪明的模型”能解决的,全部需要在投喂之前从数据侧堵住漏洞。处理过几轮之后,我养成了一个机械习惯:每次拿到一批新文件,先跑体检脚本,再看统计报告,再决定后面怎么喂。数据侧干净了,AI 侧的效果会立刻体现出来。
最后分享一个我自己操作上的小偏好。正式投喂之前,我会把“REPO_MAP + files.json 的访问说明 + 一到两个明确的业务问题”拼成一小段开篇,先让 AI 读这一段,再开始对话。不要一上来就甩问题,也不要一上来就让它读文件。给它一点“熟悉仓库”的时间,它后续给出的回答会更稳。这个顺序看似不起眼,但实际对分析质量的影响,比想象中大得多。