
在调试一个需要跨文件上下文的 bug 时我发现模型给出的补全完全是“看起来合理但实际无效”的代码。上下文窗口明明足够长文件也都被塞进了输入但它就是没有用到另一个目录里定义的函数。这个经历让我意识到一件事长上下文能力从来不是“把窗口撑大”就能解决的事关键是模型有没有在训练中见过真正需要跨长距离才能理解的结构。OctoLong 这个研究方向正是把目光从位置编码和外推技巧移开放到一个更容易被低估的地方——用跨仓库代码上下文做中期训练Mid-Training从而增强模型的长上下文建模能力。这篇文章我想把这条技术路线背后的判断、机制、实验思路以及它对代码模型和长上下文应用的启示掰开了讲清楚。1. 长上下文建模的真正瓶颈不是窗口是结构1.1 上下文窗口大不等于模型能真的读完整段输入过去几年长上下文几乎是模型迭代的关键词。从 2K、8K到 128K、1M厂商们用越来越大的位置编码、越来越聪明的注意力掩码把输入窗口不断做大。但窗口只是一个“容器”。容器变大了并不代表里面的内容会被有效理解。一个很常见的现象是把几千行代码一次性塞进上下文模型依然会把很远处的关键变量看丢。这不是注意力机制“坏了”而是模型在训练阶段根本没形成足够的激励去追踪那些相隔数万 token 的依赖关系。如果训练数据里的大量长序列只是把无关的短文本拼接在一起模型哪怕看到长输入也不会自然地产生“前面某个定义会影响后面某次调用”的建模预期。这里有一个容易混淆的点。位置编码外推、RoPE、上下文窗口扩展解决的是“能容纳多少 token”的算力问题而长上下文建模解决的是“模型愿不愿意、能不能利用远处信息”的训练问题。两者是不同层面的能力。很多评测把“能容纳”当成“能理解”于是出现了“模型能读完整本书却答不出书中两个人物在第 3 章和第 20 章之间的关系”的情况。1.2 为什么多数预训练数据其实“不够长”大模型预训练语料里确实存在很长的文本。但从结构角度看这些长文本经常不是天然连贯的长程依赖链。典型问题有三个。第一文本截断和拼接。工程上为了效率经常会把多个文档包成一个训练序列中间用特殊 token 分隔。这种“packing”策略虽然提升了训练吞吐但模型在大多数相邻 token 之间学不到真正的因果依赖反而可能在大量样本里把“跨文档无关”误当成默认模式。第二即便是一个完整文档自然语言的上下文相关性也很稀疏。一篇文章有主题但很多长段落之间并没有强制的逻辑依赖换个顺序也能读。长文本里的“有效依赖距离”比表面 token 距离短得多。第三代码和自然语言不一样。代码里的依赖是硬约束一个函数调用另一个文件里的实现一个类继承另一个模块里的基类改了一处接口下游调用链会出问题。这种依赖关系真实、可验证、不容含糊而且往往跨越很长的 token 距离。所以当我们需要模型真正擅长长上下文时不能只在推理阶段把窗口拉大也不能只靠指令微调来“提醒”它注意上下文。更好的办法是在训练过程中喂给它足够多、足够真实的“长距离因果结构”。这正是 OctoLong 标题里 Mid-Training 这一概念的价值。1.3 Mid-Training 到底处在什么位置先理清术语。预训练Pre-training是模型学习语言和世界知识的主体阶段后训练Post-training包括 SFT、RLHF目的是对齐指令和任务。Mid-Training 是一个中间阶段通常在预训练之后、或者在一个基础模型之上用特定领域数据做进一步训练让模型掌握某种预训练阶段没学好的能力。把 Mid-Training 单独拎出来很关键。它不是任务微调不追求让模型学会某一种输出格式它也不是从零开始的预训练不需要重新学通用能力。它更像是“补课”针对某个薄弱能力构造专门的数据让模型在已有基础上形成新的归纳偏好。OctoLong 选中的薄弱能力就是跨仓库代码上下文的建模。所谓跨仓库不是简单地把多个文件拼在一起而是要把一个项目里的模块依赖、一个组织里的仓库关系甚至多个独立仓库之间的共享依赖整理成模型可以学习的长上下文结构。这个选择有一个很实际的好处代码仓库本身就带有分级路径、模块边界、依赖关系图。这些信息可以自动提取不需要人工标注。比起自然语言里模糊的“长程主题”代码上下文里的依赖链更清晰、更适合作为训练信号。2. OctoLong 的思路为什么跨仓库代码上下文是关键2.1 跨仓库代码里藏着模型急需的长距离因果链OctoLong 的核心信号可以从标题里拆出来Cross-Repository Code Contexts。为什么不是单文件不是单仓库而是跨仓库单文件的上下文通常太短。大多数代码文件的长度在几百到一两千行真正跨长距离的引用有限。单仓库内多文件已经好很多但仓库内部常常有很强的局部性同一个包里的文件共享路径前缀模型很容易从路径和文件名推断出关联反而不需要真正理解深层依赖。跨仓库则不同。当一个项目依赖另一个项目、一个组织内部的多个服务通过接口通信时代码路径无法直接说明关系模型必须依靠符号名、类型定义、调用方式、配置声明等线索在很长的 token 序列里建立关联。例如service A 通过 HTTP 调用 service B 的某个 API真正把两个文件联系起来的可能是一段 proto 定义、一个 OpenAPI spec、一组序列化字段名。这些内容分散在完全不同的仓库里但它们的逻辑依赖非常强。这种结构对长上下文建模来说几乎是“教科书级别”的训练信号。模型需要学会前面的 import 会影响后面的类型推断一个接口定义可能在数万 token 之后决定另一段代码能否编译一个仓库里的 bugfix 可能改变另一个仓库里测试的预期行为。只有大量看到这类样本模型才会形成“长距离信息值得关注”的注意力先验。2.2 上下文构造从文件级到仓库级再到跨仓库要把这种思路变成可训练的数据核心问题是如何构造上下文。根据常见实践大致有三种粒度。第一层是文件级拼接。把一个仓库里的多个相关文件按依赖顺序拼起来。这种构造简单但序列的因果结构依赖文件里 import 的位置如果 import 都在开头模型只需要看前面几千 token 就能获得关键信息后面大部分内容依然是局部预测。第二层是仓库级聚合。把一个项目的核心文件、测试文件、配置文件和文档按语义相关性组成一个长文档。比文件级更接近真实开发场景因为跨文件调用是常态。但仓库内部路径隐藏了很多提示模型可能不需要真正理解调用链只看目录名就能“蒙对”。第三层是跨仓库聚合。把多个仓库中互相依赖的部分抽取出来按“依赖链”而不是“仓库边界”缝合。例如先取 service B 的接口定义再取 service A 里调用这个接口的完整函数最后取相关测试和配置。这样做出来的序列必须依赖真正的符号解析和语义关系路径前缀反而可能不一致模型只能靠理解内容来跟踪依赖。从工程实现看大致可以做成下面这种数据管线# 示例结构不是生产代码 def build_cross_repo_sequence(repos, resolver): sequence_parts [] # 1. 先解析每个仓库的依赖图 graphs [resolver.resolve_graph(repo) for repo in repos] # 2. 找到跨仓库依赖边import/API 调用/配置引用 cross_edges resolver.find_cross_repo_edges(graphs) # 3. 按依赖链抽取相关文件片段 for edge in cross_edges: provider edge.provider_file consumer edge.consumer_file sequence_parts.append(provider.context_snippet) sequence_parts.append(consumer.context_snippet) sequence_parts.append(edge.test_snippet) # 4. 组装、截断、去重 sequence concat_with_special_tokens(sequence_parts) return sequence这里的难点是依赖解析和上下文截断。跨仓库依赖可能是动态的、隐式的并非每个仓库都有干净的 package 关系。遇到没有明确依赖信息的仓库可能需要从构建日志、测试配置、版本锁定文件里去挖。OpenAI、GitHub 等生态中有大量真实公开仓库但组织级私有代码往往无法用这种方式训练这也是一个现实边界。2.3 和随机拼接的本质差异模型学到的到底是什么为什么跨仓库上下文比随机 packing 更有价值类比一下自然语言理解读一本被随机抽取的几十篇文章拼成的书你只能学会“跳过无关内容”读一本按章节推进、前后有伏笔和呼应的书你才能学会“追踪伏笔”。随机拼接的长序列对模型来说是一堆信息的并集。它确实训练了模型“在很长输入里寻找局部信息”的能力但没有训练模型“在两个远距离位置之间建立因果关系”的能力。跨仓库代码上下文则相反它让模型反复看到这样的模式一个实体在几万 token 之前定义另一个实体在几万 token 之后使用它两者之间的唯一桥梁就是符号或类型信息。模型为了降低预测损失不得不把注意力部分分配到远处的定义上。这也能解释为什么有些模型在长文本评测的“大海捞针”任务上表现很好但在真实的长文档问答和跨文件代码生成上却不理想。捞针只需要“记住远方有个针”并不需要理解针和当前文档的因果关系。而跨仓库代码建模需要的是“远方那个定义改变了当前 token 的含义”这是更难的能力也必须用更有结构的数据来训练。3. 如果你也想复现类似思路应该怎么上手3.1 先定义目标不要只盯着困惑度很多研究长上下文的工作会把 PerplexityPPL当作主要指标。但 PPL 下降只能说明模型对 token 的预测更自信了不能说明它真的用到了远处信息。它可能在“局部平滑”上变得更聪明比如通过记忆常见代码模式把困惑度降下来却没有形成跨块检索能力。更稳妥的做法是先定义一组需要跨块整合才能完成的任务并记录 baseline 模型在这些任务上的失败样例。这些失败样例就是 mid-training 前后对比的“探针”。可以参考的任务类型任务类型示例为什么能反映跨仓库建模能力跨文件补全给定仓库若干文件补全另一个文件中的函数体必须理解其他文件的类型和接口依赖追踪问“修改 A 接口后哪些调用点会受影响”需要跨仓库、跨路径建立调用链仓库级问答回答“这个服务如何鉴权”需要聚合配置、中间件和调用方代码对比测试修复给出失败测试和实现代码生成修复需要从测试断言推理出实现缺陷不建议一上来就全量做大规模训练。先准备一个小规模验证集数量不需要多但每个样本都应该有明确的跨仓库依赖关系。如果模型在 baseline 上已经能答对大部分探针说明这个问题并不难换别的任务再做。3.2 构建 mid-training 数据时最容易出问题的不是训练而是数据如果你决定做类似的实验我的建议是先花大量时间处理数据再调训练参数。数据构造有几个非常容易踩坑的环节。第一是数据泄漏。跨仓库训练数据很可能和评估集来自同一批仓库尤其是同一个开源生态内的热门项目。如果评估集也来自这些仓库模型很可能是“背过”答案而不是真正具备迁移能力。稳妥的做法是保留一组完全不参与训练的仓库作为测试集让评估目标落在跨仓库泛化上。第二是重复样本。同一个开源仓库可能被多个下游项目依赖于是相同文件会在不同“跨仓库上下文”里反复出现。如果不做去重模型会把大量容量花在记忆热点仓库上而不是学习长程依赖。去重不能只看文件级哈希还要看仓库级别的依赖图距离。第三是上下文截断。跨仓库序列可能非常长不能全部塞进模型时截断策略会影响训练信号。如果只保留 consumer 一侧附近的内容provider 的定义被截掉了模型看到的又是一个“没有原因的局部片段”。比较合理的做法是优先保留依赖链上的关键节点而不是按文件在磁盘上的顺序从头截断。一份简化的数据处理检查清单可以是用依赖解析器提取跨仓库调用边。对每条依赖边抽取 provider 定义、consumer 调用点、相关测试。去掉过于简单或过短的边。做全局模糊去重。把仓库分成训练组和评估组保证评估组不出现在训练依赖图中。对超长序列做依赖感知截断。3.3 训练与评估的排查链路如果训练后指标没有提升不要急着调学习率或训练步数。先按下面这个顺序排查。第一步看探针任务本身是否被正确喂进模型。很多“失败”其实是提示词构造问题上下文里没有包含真正相关的文件或者路径信息被截断了。先确认模型能“看到”答案所在的文件再谈“能不能利用”。第二步看数据构造是否真的引入了跨仓库信号。统计一下训练样本中provider 和 consumer 之间的平均 token 距离。如果平均距离只有几百 token那这和单文件长上下文差别不大不要指望它带来跨仓库能力的突破。第三步看 mid-training 是否破坏了原有能力。基础模型经过额外训练后有可能在通用代码补全上“灾难性遗忘”。如果你发现普通单文件任务反而变差了说明训练超参、学习率或数据比例需要调整而不是方向错了。第四步看评估指标背后的行为。除了准确率可以抽样观察注意力分布看看模型在生成某个 consumer 符号时是不是真的把注意力放到了远处的 provider 定义上。如果注意力依然只集中在局部说明训练信号没有形成主导模式可能需要增加 mid-training 的数据量或轮数。注意不要一上来就把训练步数拉满。先用一个较小模型、较小数据量验证确认探针任务有正向变化再扩大规模。跨仓库数据构造的成本往往高于训练成本。3.4 怎么判断 mid-training 是不是真的有用很多人只对比一个数字PPL 下降了多少。但更可靠的判断方式是看“跨仓库结构利用率”是否提升。可以设计一种剖面式评测同一个探针任务分别给出完整上下文、删除 provider 定义、删除 consumer 上下文三种版本。如果完整版明显优于删除版说明模型真的在利用远程依赖如果三者的结果差异不大说明模型实际上只靠局部线索就答对了你的 mid-training 并没有产生预期效果。另一个判断维度是迁移性。如果模型在训练仓库上表现好在新的、未见的跨仓库样本上也表现好说明它学到的是通用的跨仓库建模能力而不是死记特定仓库的标志。这种迁移性比训练集上的绝对指标更重要。还可以观察“错误类型”的变化。Mid-training 之前模型在跨文件补全时经常生成一个不存在的函数名Mid-training 之后虽然仍可能生成错误实现但函数名、类型、参数结构通常会更加贴近真实定义。这是一个很典型的从“局部流利”走向“结构正确”的信号。4. OctoLong 这条路对代码模型和长上下文应用意味着什么4.1 长上下文竞争正在从“窗口大小”转向“上下文利用效率”OctoLong 传递出的一个重要信号是下一代长上下文模型不能只在推理基础设施上堆算力还要在训练数据里主动制造长程依赖。位置编码、稀疏注意力、KV Cache 优化解决的是“能不能算”而跨仓库代码上下文这样的 mid-training 数据解决的是“会不会用”。这会让长上下文评估方式也发生变化。未来评估一个代码模型不应该只看它在单文件补全上的 passk也不应该只看它能读多长的文本而应该看它能否在多个仓库、多个服务、多份文档组成的长上下文里做出正确的跨模块判断。用户真正需要的是“仓库级智能”而不是“长串文本陈列”。从训练视角看跨仓库代码只是长程结构的一种来源。类似思路也可以迁移到论文引用网络、法律文书链条、软件文档与源码的对应关系等场景。凡是“信息之间存在强依赖、但物理距离很远”的领域都可能从这类 mid-training 中受益。4.2 对 RAG 和 Agent 型应用的启发如果模型本身具备跨仓库上下文理解能力很多依赖外挂检索的架构可以重新分工。现在的 RAG 系统常常是先检索一堆片段再让模型拼接本质上是在用外部机制弥补模型长程建模的不足。但当模型真的见过大量跨仓库依赖链它可以直接利用上下文里的符号关系甚至能在没有检索到正确片段时依靠长期记忆和推断给出更合理的路径。对 Agent 型工具链来说OctoLong 的思路也意味着“工具调用”可以更自然。一个能理解跨仓库上下文的模型可以自己决定读取哪个文件、看哪段配置而不是机械地按照预设步骤检索。它更接近一个真正读过整个代码库、记住了关键依赖关系的协作者。不过这一点不能过度引申。即便模型在训练时见过大量跨仓库代码真实世界的私有仓库仍然可能变化很快。mid-training 不能替代实时检索它更像是让模型拥有更强大的“长程整合骨架”具体到某次版本更新外部检索和工具调用仍然重要。4.3 适用边界不是所有场景都需要跨仓库长上下文任何方法都有边界。OctoLong 这种方向更适合代码生成、仓库级问答、复杂重构、跨服务联调分析等场景。但如果任务是短函数补全、简单片段翻译、或者只需要局部语境的代码解释过度强调跨仓库上下文可能只是增加成本和延迟收益很小。还要考虑小模型的实际收益。一个参数量不大的模型可能连基础短程依赖都没有学透这时候强行喂跨仓库长序列容易让训练不稳定甚至退化到只会“局部记忆”。更合理的是先保证模型在单文件、单仓库级别的代码理解足够扎实再考虑 mid-training 长上下文。数据获取也是限制。跨仓库依赖解析需要依赖关系图、构建信息、测试配置不是所有代码库都能轻松提取。公开代码仓库虽然多但真正干净、有明确跨仓库依赖、还允许使用的数据集并不容易构建。如果是在企业内部做还需要处理权限、隐私、合规问题这些成本和风险都要算进方案里。4.4 我的判断比起 OctoLong 本身更要关注它的数据构建哲学从我个人的经验看OctoLong 的核心贡献可能不是某个特定的模型权重而是“用真实跨仓库结构补齐长程依赖训练”这个思路。它提醒我们训练数据的结构和模型的注意力行为之间存在很强的因果关系。想在长上下文上获得真正突破不能只依赖更长的窗口和更聪明的注意力机制也要重新设计训练数据的“上下文形态”。如果你现在正在做代码模型相关工作我建议先把 OctoLong 当作一个思路来消化而不是直接照搬实验配置。可以先用少量仓库构建一个微型版本训练一个小模型再做一组跨文件探针任务看看会不会出现我们前面说的“结构正确率”提升。这个过程不一定能立刻做出惊艳的结果但它会帮助你建立起对长上下文建模的直觉。真正值得长期关注的是中期训练的数据设计未来很可能分化成很多流派。有人用代码依赖图有人用文档注解有人用 Agent 执行轨迹有人用测试反馈。OctoLong 属于代码结构派里比较自然的一支因为代码本身就把“长程因果”写在了源码里。谁能让模型更高效地从这些结构里学到长距离归纳谁就有机会在下一轮长上下文竞赛里占据主动。