跳到主要内容

智能上下文管理:项目地图与自动上下文

30 秒导读: 大项目的源码几十上百万 token,一次塞不进模型窗口。Plandex 的办法是先给整个仓库建一张 项目地图(codebase map)——用 tree-sitter 把每个文件压成"只留函数/类/类型的签名、丢掉函数体"的 目录。地图很便宜(一个大文件也许只剩几十行签名),却足够让一个叫 architect 的角色"先看目录、再决定 翻哪几本书",最后只把真正相关的少数文件全文加载进来。这就是它敢宣称"2M token 有效窗口 / 只加载需要的 东西"的底层机制。

本章只讲上下文怎么被地图化、又怎么被按需选中。它上游的规划/执行主循环见 01-tell-loop.md;architect 用哪个模型、角色怎么编排见 04-models-roles.md;用户在 CLI 里手动 load 上下文的命令见 05-diff-sandbox-cli.md


1. 这是什么(零基础也能懂)

1.1 要解决的问题:窗口装不下整个仓库

编码 agent 最尴尬的一件事:你让它"给这个项目加个功能",但项目有 800 个文件、200 万 token 的代码。 模型的上下文窗口就那么大,而且每个 token 都要花钱、都会稀释注意力。全塞进去——装不下,也烧钱; 只塞几个文件——又可能漏掉关键的那一个。

1.2 核心直觉:先给仓库做一张"目录"

想象你走进一座图书馆找资料。你不会把每本书从头读一遍,你会先看目录卡片:每本书的书名、章节标题、 作者。看完卡片,你才决定抽哪三本书出来精读。

Plandex 的项目地图就是这套卡片。对每个源文件,它不保留完整内容,只抽出签名层:

  • 有哪些函数?函数叫什么、参数和返回值是什么(但不含函数体)。
  • 有哪些类 / 结构体 / 类型?字段和方法签名是什么。
  • 有哪些顶层变量、常量、CSS 选择器、HTML 结构标签?

一个几百行的文件,压成地图后可能只剩十几行签名。整个仓库的地图,因此比全量源码小一到两个数量级。

1.3 用起来什么样

真实跑一下 Plandex 内置的 mapper(app/server/syntax/file_map/cli)去映射一个 Go 文件,输出长这样:

### /tmp/ex.go (0 🪙)

type Server struct
func (s *Server) Start(ctx context.Context) error
func New(port int) *Server

注意:Server 的字段、Start 的函数体全没了,只剩**"这个文件里有什么"的骨架。文件名后面那个 (0 🪙) 是这个文件全文加载的话要花多少 token**——地图把这个价签也标出来,好让后面的决策者按预算取舍 (这里是 CLI 直接调用、没算 token,所以显示 0;真实流程里是真实数字,见 §3.2)。

1.4 一句话类比

项目地图 = 廉价的全局索引。 就像数据库的索引:你不必扫全表,先查索引定位到少数几行,再去取那几行的 完整数据。architect 角色先"查索引"(读地图)决定该加载哪些文件,再让系统"取数据"(全文加载)。token 就这样花在了刀刃上。

本节到此为止,不碰代码细节。记住三件事:地图 = 只有签名的索引;它标了每个文件的 token 价签;有个角色 先读地图再决定拉谁的全文。


2. 顶层全景(它大概怎么转)

2.1 一张图:从源文件到"只加载需要的东西"

从上到下是数据流。左半边是建地图(把源码压成索引),右半边是用地图(architect 按需选文件)。

建地图(便宜的索引) 用地图(按需拉全文)
───────────────────── ─────────────────────
源文件 foo.go architect 角色(Context 阶段)
│ │ 读整张地图 + 目录树
▼ │ "我要动 server,那就要
① tree-sitter 解析成语法树 │ server.go 和它的类型文件"
│ MapFile() ▼
▼ ② 输出 ### Files 列表
② 只抽签名、丢实现 (文件路径放进反引号)
│ Definition{Signature,...} │
▼ ▼
③ 拼成一张大地图 ③ checkAutoLoadContext 解析路径
│ CombinedMap() + token 价签 │ 只认项目里真实存在的路径
▼ ▼
┌──────────────────────────┐ ④ Tasks 阶段:把选中文件
│ 整仓地图(几十~几百行签名) │──────────────▶ 的【全文】加载进上下文
└──────────────────────────┘ architect 读它 (地图此时退场)

怎么读这张图: 左边一次性把仓库压成地图;右边每轮对话里,architect 先读地图这张"便宜的全局视图", 挑出真正相关的文件,系统再只加载那几个文件的全文。地图是廉价的全局,全文是昂贵的局部,两者分工。

2.2 部件一句话职责

部件干什么在哪个文件
MapFile把单个文件解析成"签名列表"(FileMap)app/server/syntax/file_map/map.go
Definition一条签名记录:类型 + 签名文本 + 行号 + 子定义app/server/syntax/file_map/map.go:22
节点配置表声明式规定"哪些语法节点算定义、边界在哪"app/server/syntax/file_map/nodes_config.go
CombinedMap把多个文件的地图拼成一张,带 ### path (n 🪙) 标题app/shared/file_maps.go:9
token 计数用 tiktoken 精确数 token,或用"字节÷4"快估app/shared/tokens.go
resolveCurrentStage判定这轮走不走 Context 阶段(即要不要用地图)app/server/model/plan/tell_stage.go:22
architect prompt指挥模型"先读地图、再输出 ### Files"app/server/model/prompts/architect_context.go
checkAutoLoadContext从模型回复里捞出反引号路径、决定加载谁app/server/model/plan/tell_context.go:377
大上下文 fallbacktoken 超了就换更大窗口的模型;地图前缀做缓存app/shared/ai_models_large_context.go

2.3 主线走一遍(高层)

  1. 建地图:用户把项目加入上下文时,每个受支持的文件被 MapFile 压成签名(可并发,见 multi.go), 再由 CombinedMap 拼成一张带 token 价签的整仓地图。
  2. 判定阶段:新一轮 tell 开始,resolveCurrentStage 判断——如果开了自动上下文、且有地图,就先进 Context 阶段(Planning 阶段的第一子相)
  3. architect 读地图:此阶段只把地图 + 目录树 + 手动加载的上下文喂给模型(全文文件此时给), 模型据此产出一个 ### Files 列表,把要加载的路径放进反引号。
  4. 按需拉全文:checkAutoLoadContext 从回复里解析这些路径,只保留项目里真实存在、且还没加载的, 把它们标记为 auto-load。
  5. Tasks 阶段:进入 Planning 的第二子相,这次加载选中文件的全文(地图退场),模型基于真代码做详细规划。

3. 核心原理(逐个机制,由浅入深)

3.1 tree-sitter 项目地图:只抽签名,不抽实现

它要解决的小问题

怎么把一个几百行的源文件,自动压成"只有骨架"的几行签名?而且要支持几十种语言,不能给每种语言手写一个解析器。

思路 / 直觉

tree-sitter(一个增量式语法解析库,能把源码解析成语法树 CST)。所有语言都解析成同构的"节点树", 于是可以写一套通用的遍历逻辑:走一遍树,遇到"定义类"的节点(函数、类、类型……),只截取从开头到函数体 之前的那段文本当签名,函数体整个丢掉。

关键是"函数体从哪开始"这个边界。对每种语言,tree-sitter 给的节点类型名不一样(Go 里函数体叫 block, Python 里叫 block 但缩进不同,等等),Plandex 用一张声明式配置表把这些差异吸收掉,而不是写死 if-else。

图示:一个函数如何被"砍成"签名

源码: func New(port int) *Server { ← 签名部分,保留
return &Server{Port: port} ← 函数体(implementation boundary 之后),丢弃
}

语法树: function_declaration
├─ "func" "New" parameter_list result ← 截到这里
└─ block { ... } ◀── findImplementationBoundary 找到它,end = 它的起点

从左到右: 遍历命中 function_declaration 这个定义节点 → 找到它的"实现边界"节点(block)→ 签名 = [节点起点, 边界起点) 这段字节。函数体被整段跳过。

原理演示(示意,非源码)

# 演示:把一个定义节点压成"只有签名"。重点看 end 取在函数体开始处
def to_signature(node, source_bytes):
body = find_implementation_boundary(node) # 找函数体/类体的起点节点
start = node.start_byte
end = body.start_byte if body else node.end_byte # 截到函数体之前
return source_bytes[start:end].strip() # 这段就是签名,函数体没了

真实实现

入口 MapFile(app/server/syntax/file_map/map.go:39)先判断文件类型:不支持就返回一条 [NO MAP] 占位;Markdown 走轻量的行扫描 mapMarkdownSimple;HTML/Svelte 走专门的标记解析;其余"传统编程语言" 交给 mapNodemapTraditional

mapTraditional(map.go:151)是核心遍历:用 tree cursor 逐个走子节点,isDefinitionNode 判断是不是 定义,是的话调 findImplementationBoundary(nodes_find.go:153)定位函数体起点,签名就取 node.Bytes[start:end](map.go:281-312)。若这个定义还能嵌套(如类里有方法),isParentNode 为真时会递归进函数体继续抽子定义(map.go:318-323),于是类/方法形成父子结构 Definition.Children

值得注意的一处刻意取舍:抽取时故意不保留前置注释(map.go:333-334,getPrecedingComments 被注释掉),原因写在代码旁——"no comments for now to minimize tokens"。地图的每一 token 都要省。

配置驱动:一张表吃下 30+ 种语言

判断"什么节点算定义、边界在哪"全靠 nodes_config.go 里的一组 nodeMap。每张表把 节点类型名 → 配置,配置支持三种匹配(nodes_config.go:9-13):精确、前缀、后缀。例如 definitionNodeMap"_definition"后缀匹配all: true(nodes_config.go:265-268), 一举命中各语言里所有 *_definition 节点。语言专属的差异则用 languages / except 字段限定。

getConfig(nodes_find.go:102)是查表逻辑:先找精确匹配,再找前缀/后缀匹配,ignore 优先级最高。 几张关键的表:

表(nodes_config.go)管什么
definitionNodeMap哪些节点算"一条定义"
parentNodeMap哪些定义可以嵌套子定义(递归进去)
implBoundaryNodeMap"函数体/实现"从哪个节点开始(签名截到这)
assignmentNodeMap + assignmentBoundaryNodeMap变量/常量赋值,签名截到 = 之前
identifierNodeMap找标识符(取名字用)

个别语言无法用节点类型区分,就特判。例如 Elixir 里函数、宏、模块全是 call 节点,setNodeType (nodes_find.go:295-331)靠内容前缀(defmodule / defmacro / defp / def……)把一个 call 细分成十几种定义类型。

支持的语言列在 app/shared/syntax.go:46Languages(共 33 种:Go/Python/TS/Rust/Java/C++/Ruby/PHP/ Elixir/Swift/Kotlin……),对应克隆里 app/server/syntax/file_map/examples/ 下每种语言一个样例文件。

关键细节 / 坑

  • 配置文件不建地图,交给模型看文件名判断。 YAML/TOML/JSON/HCL/CUE/Protobuf 被列进 lacksFileMapSupport(syntax.go:82-94),地图里只留 [NO MAP]。注释解释:配置文件的"签名"没意义, 让模型按文件名决定要不要加载(architect prompt 也专门叮嘱了这点,见 §3.4)。
  • Markdown 不走 tree-sitter(SkipTreeSitter,syntax.go:96-98),而是逐行扫 # 标题当"签名", 因为对 Markdown 来说标题层级就是最好的骨架(map.go:465 mapMarkdownSimple)。
  • HTML 只留"语义骨架"。 markup.go 只保留 main/nav/section/form/table/div/ul...significantHtmlTags(markup.go:136),还会把重复的兄弟标签合并[8x]div (consolidateRepeatedTags,markup.go:85)——一个 100 项的列表不会在地图里铺 100 行。

3.2 地图拼装与 token 价签

它要解决的小问题

单文件地图有了,怎么把整个仓库几百个文件的地图拼成一张给模型看?而且要让模型知道每个文件全文加载值多少 token

思路

按路径排序后,逐个文件写一个 ### 路径 (token数 🪙) 的标题,底下跟这个文件的签名。空的就写 [NO MAP]。 那个 token 数是文件全文的 token 成本——因为 architect 要用它做预算:一个 5 万 token 的大文件,值不值得 为了当前任务整个加载进来?

真实实现

CombinedMap(app/shared/file_maps.go:9)干这件事:排序路径 → 对每个路径调 MapFileHeading(path, tokensByPath[path]) 生成 ### path (n 🪙) 标题 → body 为空则写 [NO MAP]。 标题格式就一行:fmt.Sprintf("\n### %s (%d 🪙)\n\n", path, tokens)(file_maps.go:31-33)。

那个 token 价签怎么来的?顺着 mapTokens 往上游追:它最终来自 getMapFileDetails(app/cli/lib/context_shared.go:34)。文件被完整读入(可能截断到 500KB)后, 用 GetNumTokensEstimate(res.mapContent)(context_shared.go:96)数全文的 token。所以地图里每个 🪙 = 该文件全文加载的真实代价。这正是 architect 做取舍的价目表。

多文件映射是并发的:ProcessMapFiles(app/server/syntax/file_map/multi.go:21)用"CPU 数一半"个 worker 并行跑 MapFile;超过 MaxContextMapSingleInputSize(500KB)的文件直接标 [NO MAP - TOO LARGE] (multi.go:44-50),不浪费算力去解析巨型文件。

拼好的整仓地图,最终作为一个 ContextMapType 的上下文,Body 就是 CombinedMap 的结果 (app/server/db/context_helpers_load.go:200 combinedBody := mappedFiles.CombinedMap(mapTokens))。

3.3 token 计数:精确 vs 快估

地图和上下文的大小都要能被量化,靠 app/shared/tokens.go 两条路:

函数(app/shared/tokens.go)怎么算何时用
GetNumTokensEstimate用 tiktoken 的 gpt-4o 编码器真的编码一遍数长度精确、需要准数时(如地图 token 价签)
GetFastNumTokensEstimate / GetBytesToTokensEstimate字节数 ÷ 4(EstimatedBytesPerToken = 4)快、大文件或截断时的粗估

编码器在 init 里初始化(tokens.go:13-19),取 gpt-4o 的 BPE。这个数字贯穿全局:它是 architect 的预算单位, 也是后面 §3.5 判断"要不要换大窗口模型"的输入。

3.4 自动上下文:architect 先读地图、再宣布要加载谁

这是"只加载需要的东西"落地的地方。它是一个**两阶段(two-phase)**流程,都发生在 Planning 阶段内部。

阶段一:Context 阶段——"决定并宣布"

什么时候进这个阶段? resolveCurrentStage(tell_stage.go:22)判定:只有当 req.AutoContext && hasContextMap && !contextMapEmpty && !wasContextStage 全为真时,才把 PlanningPhase 设成 PlanningPhaseContext(tell_stage.go:86-89);否则直接进 PlanningPhaseTasks。 换句话说:开了自动上下文、且有非空地图,才走"先读地图"这一步。

这一阶段喂给模型什么? 只喂地图 + 目录树 + 手动加载的上下文,不喂任何全文文件。看 formatModelContext:Context 阶段里,pending 的文件只以"这个文件有改动 (n 🪙)"一行出现、不含内容 (tell_context.go:239-243),注释解释得很直白——不这么做的话"the context phase can get overloaded with pending file content"。地图则通过 includeMaps: true 放进去(tell_exec.go:224-230)。

指挥模型的 prompt。 GetArchitectContextSummary / GetAutoContextTellPrompt (prompts/architect_context.go)把模型设定成"专家软件架构师",要求它:

  1. 先用地图做一个高层次、面向架构的概览(architect_context.go:37-41);
  2. 然后在同一条回复里接着输出 ### Categories(相关类别)和 ### Files(相关文件+符号), 最后打一个 <PlandexFinish/> 标签收尾(architect_context.go:44-61)。

prompt 里有几条硬约束值得记:

  • 只能列地图里真实存在的路径——反复强调"Do NOT guess file paths",禁止臆造不存在的文件 (architect_context.go:87115),还给了 GOOD/BAD 对照例子。
  • 文件路径必须放进反引号(architect_context.go:213),因为下游就靠反引号来提取路径。
  • 给了 token 预算:告诉模型下一阶段的 tokenLimit,并教它权衡"文件重要性 vs token 大小"、 按重要性排序,系统会从前往后加载直到触限(architect_context.go:225-237)。
  • [NO MAP] 不代表不重要:提醒模型对没有地图的文件(如配置文件),要按文件名和路径判断相关性 (architect_context.go:239-240)。
  • 还有一组关系规则(Interface & Implementation / Reference / API Client Chain / Database Chain / Utility)教模型"加载实现就要连带加载接口、类型、测试、工具文件"(architect_context.go:243-299), 目的是尽量不漏相关文件。

阶段二:解析路径、按需加载全文

模型回复完,checkAutoLoadContext(tell_context.go:377)接手:

# 示意,非源码:从模型回复里捞出要加载的文件
paths = regex_find_all("`(.+?)`", reply) # 抓所有反引号里的东西
for p in paths:
if p in project_paths: # 只认项目里真实存在的路径
activate(p) # 标记为"本轮激活"
if p not in already_loaded:
auto_load(p) # 还没加载的,排队加载全文

真实逻辑:用 pathRegex(一个匹配"反引号包裹内容"的正则,tell_context.go:368)抓出所有反引号内容,只保留 req.ProjectPaths[trimmed] 为真(即项目里真实存在)的路径,填进 toAutoLoad / toActivate (tell_context.go:406-425)。臆造的路径因为不在 ProjectPaths 里,自动被丢弃——这是对"模型可能瞎编 文件名"的最后一道防线。

之后进入 Tasks 阶段,formatModelContext 这次用 activeOnly: true + activatePaths,并按 activatePathsOrdered 的顺序、在 maxTokens 预算内加载选中文件的全文(tell_exec.go:257-265)。 地图此时 includeMaps: false 退场。至此,"先看目录、再翻书"闭环完成。

关键细节

  • 实现阶段根本不给地图。 当 tell 进入 TellStageImplementation,formatModelContextincludeMaps: false(tell_exec.go:216-220)——真正写代码时看的是已选中的全文,不是索引。
  • 地图为空就跳过整个流程。 contextMapEmpty 为真时不进 Context 阶段(tell_stage.go:86), 退化成普通规划。

3.5 大上下文与上下文缓存:把地图当作可缓存的稳定前缀

2M "有效窗口"从哪来

Plandex 说的大窗口,一部分是模型本身的大窗口:内置模型表里 Gemini 1.5 Pro 的 MaxTokens: 2000000(app/shared/ai_models_available.go:264)。但更关键的是它不会真去填满 2M——地图机制 让它大多数时候只用一小部分,只在需要时才拉全文。

token 超了怎么办:按输入大小换更大的模型

GetRoleForInputTokens(app/shared/ai_models_large_context.go:49)在真正发请求前算好输入 token (还乘一个 padding 系数留余量,ai_models_large_context.go:57),如果超过当前模型的 MaxTokens, 就顺着 LargeContextFallback逐级换到更大窗口的模型,直到装得下或到链尾(ai_models_large_context.go:60-75)。 输出侧有对称的 GetRoleForOutputTokensmaxFallbackDepth = 10 只是防循环引用的保险 (ai_models_large_context.go:3)。fallback 链具体怎么配、用哪些模型,见 04-models-roles.md

上下文缓存:地图是稳定前缀,值得缓存

Context 阶段和 Tasks 阶段共享同一段前缀——地图 + 手动上下文。Plandex 特意先构造这段"共享部分"并打上 缓存标记:planStageSharedMsgsbaseOnly: true + cacheControl: true 生成 (tell_exec.go:224-230),formatModelContext 末尾据此在最后一个 part 上挂 CacheControl: Ephemeral(tell_context.go:347-351)。系统提示词侧也大量使用 ephemeral 缓存标记 (app/server/model/plan/tell_sys_prompt.go:69 起)。

为什么这样有效: 支持 prompt caching 的模型(内置表里多数带 SupportsCacheControl: true, 如 ai_models_available.go:146)会缓存这段稳定前缀。地图内容在一轮对话内基本不变,于是两个阶段之间 这段大前缀不用重复计费/重算。为让缓存命中,加载顺序还被刻意做成稳定排序(无激活顺序时按 Name 排, tell_context.go:194-201,注释:"stable order for caching")。

配套的一组尺寸红线在 app/shared/context.go:12-22:单个映射文件上限 500KB(MaxContextMapSingleInputSize)、 最多映射 3000 个路径(MaxContextMapPaths)、映射批处理 10MB/500 文件一批。这些护栏保证地图本身不会 反过来把窗口撑爆。


4. 巧妙之处(可借鉴的技术)

  • "签名即索引" —— 用 tree-sitter 只截到函数体之前,就得到一个比全文小 1~2 个数量级、却保留全部 "有什么"信息的索引。比 embedding 检索更确定、可读、可被模型直接推理。核心在 map.go:281-312end = body.TsNode.StartByte()
  • 配置驱动跨 30+ 语言 —— 不给每种语言写解析器,而用 nodes_config.go 的前缀/后缀匹配表 + 少量特判 (Elixir setNodeType),一套遍历吃下所有语言。加新语言多半只是加几行配置。
  • 地图自带 token 价签 —— ### path (n 🪙) 里的数字是全文加载成本,直接把预算信息喂给决策者, 让"要不要加载大文件"变成模型能算的账(file_maps.go:31)。
  • 臆造路径的双重防线 —— prompt 反复禁止编造路径,代码再用 req.ProjectPaths 过滤一遍 (tell_context.go:412),模型即使幻觉出不存在的文件也无害。
  • 把地图做成可缓存前缀 —— 用 cacheControl: Ephemeral + 稳定排序,让两阶段之间的大前缀走 prompt cache,省掉重复成本(tell_exec.go:229tell_context.go:194-201)。
  • 省 token 的偏执 —— 连"要不要保留前置注释"都为省 token 砍掉(map.go:333-334),HTML 重复标签 合并成 [8x](markup.go:116-119)。

5. 边界与局限

  • 配置文件没有地图。 YAML/TOML/JSON/HCL/CUE/Protobuf 只显示 [NO MAP],模型只能靠文件名猜相关性 (syntax.go:82-94)。若关键信息藏在一个命名不直观的配置文件里,可能被漏掉。
  • 地图依赖解析成功。 tree-sitter 解析失败会走 fallback parser,再失败就返回空定义 (map.go:112-131);语法有严重错误的文件可能地图为空。
  • 单文件 500KB 截断。 超过 MaxContextMapSingleInputSize 的文件被截断或标 [NO MAP - TOO LARGE] (multi.go:44context_shared.go:58-61),超大生成文件在地图里近乎不可见。
  • 选文件的准确性取决于模型。 architect 漏选一个真正相关的文件,后续规划就缺料;prompt 里那一大堆 "关系规则"正是为压低这种漏选率而堆的启发式(architect_context.go:243-299),但不是保证。
  • 地图新鲜度靠缓存失效。 地图按文件内容 SHA 缓存(GetCachedMap,context_helpers_map.go:14); 文件变了但缓存没更新时,可能读到旧签名。
  • 注释被丢弃。 为省 token 不保留 docstring/注释(map.go:333-334),那些只写在注释里的语义 (如"这个函数已废弃")不会进地图。

6. 横向对比

同为编码 agent,处理"大项目装不下窗口"各有取法:

方案核心手段与 Plandex 地图的差异
Plandex(本章)tree-sitter 签名地图 + architect 按需选文件确定性索引,模型显式决策,无向量检索
Aider 的 repo map也用 tree-sitter 抽签名,但配 PageRank 按引用重要性排序Aider 自动排序节点重要性;Plandex 交给 architect 用 token 预算取舍
RAG / embedding 检索把代码切块做向量相似度召回召回是概率性的;Plandex 的地图是可读、可核对的显式结构

Plandex 的取舍很清楚:用一张人也能读、模型能推理的显式地图,把"加载谁"做成一次可审计的模型决策, 而不是黑盒相似度。代价是准确性依赖 architect 的判断和 prompt 里那套关系启发式。

7. 代码地图(导航索引)

按符号名 grep 定位(比行号抗漂移)。路径含 app/ 前缀,相对克隆根。

主题文件路径符号名
单文件映射入口app/server/syntax/file_map/map.goMapFile
签名记录结构app/server/syntax/file_map/map.goDefinition
传统语言遍历/砍签名app/server/syntax/file_map/map.gomapTraditional
Markdown 轻量映射app/server/syntax/file_map/map.gomapMarkdownSimple
地图渲染成文本app/server/syntax/file_map/map.go(*FileMap).String
找函数体边界app/server/syntax/file_map/nodes_find.gofindImplementationBoundary
节点分类查表app/server/syntax/file_map/nodes_find.gogetConfig
Elixir 节点特判app/server/syntax/file_map/nodes_find.gosetNodeType
定义/父/边界配置表app/server/syntax/file_map/nodes_config.godefinitionNodeMap / parentNodeMap / implBoundaryNodeMap
HTML 语义骨架app/server/syntax/file_map/markup.gomapMarkup / significantHtmlTags / consolidateRepeatedTags
并发映射多文件app/server/syntax/file_map/multi.goProcessMapFiles
拼装整仓地图app/shared/file_maps.goCombinedMap / MapFileHeading
token 计数app/shared/tokens.goGetNumTokensEstimate / GetBytesToTokensEstimate
支持语言清单 / 不映射清单app/shared/syntax.goLanguages / lacksFileMapSupport / HasFileMapSupport
地图 token 价签来源app/cli/lib/context_shared.gogetMapFileDetails
组装地图上下文app/server/db/context_helpers_load.gocombinedBody (调用 CombinedMap)
判定是否走 Context 阶段app/server/model/plan/tell_stage.goresolveCurrentStage
格式化并挑选上下文app/server/model/plan/tell_context.goformatModelContext
解析反引号路径、按需加载app/server/model/plan/tell_context.gocheckAutoLoadContext / pathRegex
两阶段上下文装配 + 缓存标记app/server/model/plan/tell_exec.goplanStageSharedMsgs (cacheControl: true)
architect 指挥 promptapp/server/model/prompts/architect_context.goGetArchitectContextSummary / GetAutoContextTellPrompt
大上下文模型 fallbackapp/shared/ai_models_large_context.goGetRoleForInputTokens
上下文尺寸红线app/shared/context.goMaxContextMapSingleInputSize / MaxContextMapPaths
2M 窗口模型 / 缓存支持app/shared/ai_models_available.goMaxTokens / SupportsCacheControl