资讯动态

给AI喂代码前先画一张仓库地图:从全量投喂到精准索引

发布时间:2026/9/16 3:24:54 来源:尧图企业网站定制
上周我把公司那个累计超过一万个源文件的仓库喂给AI请它出一份架构评审意见。第一版输出看起来非常专业结果对照代码一查至少三处模块归属是错的还有两个接口名干脆是编的。问题不在模型而在我自己我把仓库当成了一个可以整包消化的文件集但AI真正需要的是一张地图。在把近万个源文件交给AI之前我先做了一件事——做了一份“仓库地图”把文件清单、符号表、依赖关系、构建命令、关键路径整理成一份可检索、可导航、大小可控的索引。这篇文章就围绕这件事展开讲清楚我为什么这么做、具体怎么做的、以及过程中踩过哪些坑。如果你正准备做代码库问答、AI辅助架构评审、Code Review或者想给Agent接一个真实仓库这篇东西应该能帮你少走不少弯路。1. 全量喂源文件为什么必翻车我算清了一笔Token账1.1 近万个文件到底有多大先用一张草稿纸算清楚我第一版方案是简单粗暴地把所有源码拼成一个“超级文档”喂给模型结果还没跑完就觉得不对劲。按最保守的口径估算一万个文件平均每个文件200行每行大约折合5到8个token仅源码就有1000万到1600万token再算上文件路径、分隔符、帮助模型理解的注释标记实际消耗还要上浮20%。这个量级意味着什么哪怕用的是支持百万级上下文窗口的模型也远远装不下整个仓库。更扎心的是源码本身就是低信息密度的。一万个文件里掺杂着空行、历史遗留代码、自动生成的模板、各种IDE配置真正有业务含义的内容占比远没有想象中高。你把整栋图书馆的书全堆在一个人面前他并不会因此变成馆员只会被纸山淹没。AI也一样上下文不等于理解力塞得越多它反而越难抓住关键线索。1.2 上下文窗口不是越大越好检索噪音会把模型带偏很多人会想既然模型支持长上下文那我多喂一点总没错吧实测下来恰恰相反。把大量无关文件放进去之后模型会被中间那些“看起来像那么回事”的代码带偏。我遇到过它把两个同名Service搞混也遇到过它根据一个废弃配置类推断出完全错误的技术栈。这里可以打个比方给一个刚入职的同事塞一个10万行代码的压缩包不如给他一份模块地图加20个关键文件。新人需要的不是全部代码而是先知道仓库里有什么、各自干什么、谁依赖谁、入口在哪。模型面对一个陌生仓库时和新人没有任何区别。它真正缺的不是信息总量而是信息的组织方式。1.3 IDE报错和LLM幻觉其实是同一个病根很多人在日常开发里都见过这类报错VS提示“无法打开源文件”Java编译器说“在源文件中未声明类”Qt工程报“无法打开源文件 ui_confirm_d.h”。第一次遇到会觉得莫名其妙文件明明就在磁盘上工具怎么就是找不到后来才明白这些报错的本质都是同一个——工具链没有拿到正确的文件定位信息。要么是include路径没配要么是源文件没有被纳入编译单元要么是文件相对路径对不上。LLM全量吞代码产生的幻觉病根一模一样。它没有一份“该从哪里读、读到什么程度、哪些文件是同一模块”的导航信息于是只能在概率空间里脑补。我后来把这两件事放在一起想通了AI读代码之前必须先掌握“定位信息”这个定位信息就是仓库地图。2. 我先做的事给仓库画一张“可导航地图”而不是倒文本2.1 仓库地图不是目录树是一份有方向的索引第一次做地图时我偷懒直接tree了一把把输出丢给模型结果依然不理想。回想起来很正常目录树只是文件名的堆叠没有任何语义。它告诉模型“有哪些文件”却没告诉它“这些文件是干什么的、边界在哪里、入口是什么”。真正的仓库地图要能回答下面几类问题这个仓库分成哪几个模块模块边界画在哪每个模块的入口文件是哪个对外暴露了哪些核心符号模块之间谁依赖谁改动某个核心包会影响哪些下游构建命令、测试命令、启动命令分别是什么哪些路径是生成代码或第三方代码AI千万不要去改这些问题直接决定了AI回答问题的质量。没有答案它就只能瞎猜有了答案它才能把问题落到具体文件和具体符号上。2.2 我最终落地的三层地图结构我最终实际使用的地图分三层分别服务不同粒度的检索需求。第一层是仓库级导航相当于整本手册的目录页第二层是模块卡对应每个子系统的概览页第三层是文件条目对应每个源文件的“名片”。下面是简化的样子## 仓库: order-platform (Java Python 混合) 技术栈: Spring Boot, MySQL, Redis, Python FastAPI 构建: ./gradlew build 测试: ./gradlew test / pytest tests/ 模块: - order-service: 订单核心依赖 user-service、payment-service - payment-service: 支付渠道对外提供 PaymentService.create_order - user-service: 用户与权限提供 UserContext、AuthFilter 关键入口: - src/order/controller/OrderController.java - src/payment/service/PaymentService.java第一层给AI一个整体定位第二层让它判断问题该落到哪个模块第三层才是具体文件路径和关键符号。三层结构有一个好处AI不需要一次性读完全部细节而是可以像人一样先看总览再根据问题层层下钻。这个“下钻”的动作非常关键后面讲喂养策略时还会提到。 ### 2.3 地图不是一次性文档而是持续更新的活索引 我刚做完第一版地图时很兴奋觉得以后喂AI之前跑一遍就行。结果仓库一周内改了一百多个文件地图还在描述旧结构AI给出的建议全部作废。从那以后我把地图生成脚本挂到了CI里每次主分支有新提交就自动重新生成并在地图文件里写入本次对应的commit hash。 这件事看起来不起眼但决定了整套方案能不能长期跑下去。一个过期地图比没有地图更糟糕因为AI会非常自信地按照旧路径找文件然后反复碰壁。地图必须是活文档最好是机器生成、机器更新、人工抽查而不是靠某个人手工维护。 ## 3. 地图生成实操从盘点、抽符号到压缩摘要的完整链路 ### 3.1 第一步安检先把真正需要喂给AI的文件筛出来 建地图的第一步不是抽取信息而是做减法。仓库里真正值得进入地图的是“人写的、有业务逻辑的、会被后续修改的”源码文件其余的大多是噪音。我默认排除的目录包括 .git、node_modules、dist、build、target、venv、__pycache__、.idea、.vscode以及各种自动生成的代码和二进制文件。 筛选脚本用Python写非常简单核心思路是遍历目录时直接跳过排除项只保留常见源码后缀 python import ast import re from pathlib import Path EXCLUDE_DIRS { .git, node_modules, dist, build, target, venv, __pycache__, .idea, .vscode, vendor } CODE_EXTS { .py, .java, .js, .ts, .go, .rs, .cpp, .c, .h, .hpp, .cs, .php, .rb, .kt, .swift } def iter_code_files(root: Path): for p in root.rglob(*): if any(part in EXCLUDE_DIRS for part in p.parts): continue if p.is_file() and p.suffix in CODE_EXTS: yield p if __name__ __main__: files list(iter_code_files(Path(.))) print(ftotal code files: {len(files)})这个阶段还应该顺手把“文件大小异常”的挑出来。单个文件超过几百KB大概率是生成代码、数据文件或者历史遗留文档我会单独标记不让它影响后续统计。安全也是为了后续的符号提取和依赖分析更干净。3.2 第二步抽符号让每个文件变成一张“名片”筛选完文件之后第二步是给每个文件生成一张“名片”。名片上不需要包含源码全文只需要记录几个关键字段文件路径、语言、公开类/函数/接口的签名、顶部注释、导入的依赖、以及TODO/FIXME标记。AI拿到这些信息就能判断“这个文件是干什么的”而不需要在一开始就读完整个文件。抽取方案按语言区分。Python有现成的ast模块可以直接解析出类、函数、导入Java、C、Go这些可以用Universal Ctags或Tree-sitter不想引入太多工具时用正则先把import、require、include抓出来也够用。我的实际做法是“先抓能抓的再慢慢补精确解析”而不是一开始追求完美解析所有语言。下面是一个极简的Python示例展示用ast提取符号的样子def extract_python_symbols(path: Path) - dict: try: tree ast.parse(path.read_text(encodingutf-8, errorsignore)) except SyntaxError: return {classes: [], functions: [], imports: []} classes, functions, imports [], [], [] for node in ast.walk(tree): if isinstance(node, ast.ClassDef) and not node.name.startswith(_): classes.append(node.name) elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): if not node.name.startswith(_): functions.append(node.name) elif isinstance(node, ast.Import): imports.extend(alias.name for alias in node.names) elif isinstance(node, ast.ImportFrom): imports.append(node.module or ) return {classes: classes[:50], functions: functions[:100], imports: imports[:100]}注意这里我对函数和类的数量做了截断避免某些“上帝文件”让地图失去代表性。一个文件有200个函数时AI真正需要知道的只是前几十个公开接口而不是全部。3.3 第三步依赖方向找出真正的核心模块有了每个文件的“名片”下一步是统计依赖关系。我要回答的问题是整个仓库里哪些模块被依赖得最多哪些模块是底层基础设施哪些模块是业务入口这决定了AI回答问题时的优先级——被大量依赖的模块往往是一改就引发连锁反应的核心。依赖分析不需要引入重量级工具。先把每个文件的import/require/include解析出来转成“源文件 → 目标目录/模块”的映射再按目录聚合统计被引用次数import re from collections import defaultdict, Counter IMPORT_PATTERNS { .py: r^\s*(?:import|from)\s([\w\.]), .java: r^\s*import\s([\w\.]);, .js: r^\s*(?:import|require)\s*\(?[\]([^\]), .go: r^\s*\([\w/\.\-])\, } def extract_imports(path: Path): suffix path.suffix.lower() pattern IMPORT_PATTERNS.get(suffix) if not pattern: return [] try: text path.read_text(encodingutf-8, errorsignore) except Exception: return [] return re.findall(pattern, text, flagsre.MULTILINE) dep_counter Counter() for p in iter_code_files(Path(.)): for imp in extract_imports(p): target imp.split(.)[0] # 粗粒度放到顶层包 if target and target not in EXCLUDE_DIRS: dep_counter[target] 1这一步产出的“被依赖Top 50”非常有用。它能直接回答“我想改common包到底会影响多少模块”这类问题。有了这份依赖方向地图才真正变成了有方向的索引而不是一堆平铺的文件名。依赖分析不需要绝对精确漏掉一些动态导入完全可以接受相比精确性稳定性更重要宁可每次都漏同一批也不要一次精确一次漏。3.4 第四步生成压缩摘要并控制Token预算所有中间信息收集完之后最后一步才是生成人类和模型都可读的压缩摘要。压缩摘要不代表删代码而是把每个文件的信息压缩成固定格式。我的目标是整个仓库的地图控制在1万到3万token以内让AI可以整体放进上下文。我最终保留两个产物repo_map.json给机器用AI_README.md给人用也给模型用。AI_README.md的核心结构如下## 模块: payment-service (35 files) 依赖: order-service, user-service 对外符号: PaymentService.create_order, RefundService.refund 关键文件: - src/payment/service.py | 4KB | 接口层创建支付单 - src/payment/worker.py | 8KB | 异步任务对账这个阶段的“压缩”是信息重组不是粗暴截断。每个模块卡最好由人工review一遍因为脚本能告诉你模块里有哪些符号但“这段代码在业务上到底负责什么”这种语义信息脚本给不出来。人工review的成本也不算高近万个文件的仓库最后浓缩成几十个模块卡每张卡花两三分钟一两小时就能完成。但这一两小时换来的是之后AI问答不再胡编乱造的底气。4. 把地图教给AI的正确姿势系统提示词里的“导航栏”4.1 地图应该挂在系统提示词里而不是丢进对话正文地图生成好之后喂给AI的方式同样有讲究。我的建议非常明确把地图放在系统提示词里而不是塞进用户消息正文。原因是系统提示词会在整个会话中持续生效模型每次回答时都带着它而正文里的地图一旦被后续内容冲淡模型很容易“忘记”自己手上有地图这回事。我实际使用的提示词框架大概是这样的你是一个资深软件架构师。下面是一份当前代码仓库的地图。 在回答任何问题前必须先做“定位”把问题映射到地图中的模块和文件路径。 如果你需要查看具体文件内容使用工具 read_file(path)。 禁止直接根据地图编造代码细节禁止使用不在地图或工具返回结果中的符号名。 repo_map {repo_map_text} /repo_map核心不是让模型背地图而是逼它先做“定位”再回答。这样哪怕它最终给的建议不够好至少走到“去翻真实文件”这一步时路径和符号都是真的。4.2 按需拉取策略让AI虚实结合而不是一次读完地图进去之后实际使用中我还用了一套“虚实结合”的策略。所谓虚是让AI基于地图做判断和规划用模块名、文件路径、符号名把答案框架搭出来所谓实是当模型需要确认具体代码逻辑时通过工具调用去读取真实文件内容。这套组合拳既控制了token消耗又保证了答案有真实依据。具体落地有三种常见方式。第一种是给Agent配read_file(path)和grep_symbol(name)两个工具让它自己按地图索引去读真实文件这是最省token也最准确的方式适合做长期代码助手。第二种是只做纯RAG把地图作为全局知识、把源码分块作为检索库但注意在检索结果里保留“路径回链”让模型知道每个片段出自哪个文件否则容易出现张冠李戴。第三种是多轮人机协作先让AI读地图产出“问题-文件映射表”人工确认后再让AI读取具体文件。第三种适合一次性架构评审虽然慢但每步都可控。我在实际项目里优先用第一种因为它最符合“先定位再细读”的思考方式。模型先看地图锁定模块再根据模块卡里列出的关键文件缩小范围最后用read_file读取真正需要的文件整个链条和人类工程师的工作方式几乎一样。4.3 同一份仓库同一组问题有地图和没地图的差异为了确认这套做法不是心理安慰我用同一个仓库、同一组20个问题做过一次对比实测。没有地图时AI平均每个问题消耗约12万token回答里有具体文件路径的只有6次能对上真实符号名的只有3次。有地图之后完整跑完20个问题大约消耗4万token路径和符号全部可以追溯到真实文件并且它能在回答里主动区分“这是确认过的”和“这个还需要查证”。这个对比本身说明两件事:第一不带地图的全量喂法大部分token都浪费在无关文件上第二高质量答案最重要的不是更多上下文而是更准确的定位。地图本质上是用很小的token开销换取了AI对代码库结构的“先验知识”这笔账在任何新增代码超过一定规模的仓库里都是划算的。5. 地图维护与踩坑清单源文件喂AI前的最后几道坎5.1 索引漂移地图过期比没有地图更危险地图最大的敌人不是生成难度而是过期。我有一次让AI定位登录模块它反复去查auth_controller.py但那个文件在两周前就已经被重命名为session_controller.py旧路径早就不存在了。AI非常自信地把旧路径当正确答案整段分析建立在虚无之上这就是“索引漂移”的典型事故。应对方法我从三个层面做第一生成脚本挂进CI主分支每次有提交都重新生成地图保证它和代码同步第二地图文件头部写入“最后生成时间”和“对应commit hash”让AI能感知时效性第三给Agent配上search_symbol工具让它在万一遇到地图偏差时可以实时探测真实符号。地图可以旧但AI必须有能力发现“旧了”而不是硬着头皮信任一个已经失效的索引。5.2 多语言混仓与解析失败别指望一个工具通吃大型仓库很少是单一语言Java、Python、JS、SQL、Protobuf混在一起是常态。没有任何解析器能完美覆盖所有语言我在实际处理中碰到过三种情况Python文件语法不标准导致ast解析失败、Java代码里有复杂的注解导致正则提取不准、C头文件依赖宏定义导致include关系看起来很奇怪。我的对策是按扩展名分派不同的解析策略能精确解析的语言用标准解析器不能精确解析的就退化为“读取前200行注释提取import/include抓函数签名”并把所有“解析失败文件”单独放进一个清单写进地图的附录。这一步非常重要——如果地图里静默丢失了某些文件AI会以为这个仓库里根本没有那些文件这种“缺失的幻觉”比没有地图更隐蔽。5.3 敏感信息脱敏喂给AI前的最后一关仓库全量喂给AI之前还有一道必须过的坎敏感信息脱敏。代码里藏着密钥、内网地址、个人手机号、证书私钥简直太常见了尤其是有历史包袱的老仓库。我见过有人把带着数据库密码的配置文件直接拼进提示词幸好是在内网模型上跑否则这条密码就进了模型提供方的日志。所以在生成地图之前我会先跑一轮敏感信息扫描把这些模式过滤或打码import re PATTERNS [ rAKIA[0-9A-Z]{16}, r-----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY-----, r(?i)(password|passwd|secret|token)\s*[:]\s*[\][^\][\], r(?:https?://)?(?:10\.\d{1,3}|192\.168\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.)\d{1,3}, ]扫描结果里出现的文件我会在进入地图前做进一步处理要么从喂给模型的文件集中移除要么把敏感值替换成占位符。这一步不能省尤其是准备使用任何外部模型服务时它直接决定了你这套AI代码分析方案能不能安全落地。5.4 地图之外我还保留了三个小习惯第一个习惯是给地图加上“使用说明”。我在AI_README.md开头专门写了几句话告诉AI先看模块划分再定位问题最后再决定是否读全文。这个说明看起来多余却能显著减少模型一上来就乱读文件的行为。第二个习惯是把超大文件单独拎出来处理。仓库里总有几个动辄几千行的“上帝文件”它们塞进任何地图条目都不合适。我默认把它们排除在模块卡之外单独生成一个“超大文件清单”里面只写路径、行数、大致作用。等AI真正需要分析时才按需读取不为它们浪费地图的token额度。第三个习惯是定期抽查地图质量。我每周会随机挑三个模块卡对照真实代码看描述是否准确。这个动作不费时间但能发现很多自动化流程查不出来的语义偏差。毕竟地图最核心的价值是“语义正确”而语义这种东西脚本只能帮你整理素材最终还是需要人来做判断。上面这套流程跑下来我才真正理解那句话在把代码喂给AI之前你其实是先给AI当了一回导航员。近万个源文件不是问题问题是你有没有把最珍贵的信息——结构、边界、依赖、入口——从代码噪音里提出来。地图做完之后AI不再是面对一堆陌生源码的实习生而是一个拿着公司内部Wiki开始工作的老员工。这个差别用过一次就回不去了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价