上下文塞满 vs 塞对:两种策略的实际差距

wescode · 2026-09-29 · 上下文 / CKG / 记忆 / 对比

利益声明:本文作者参与了 wescode 的开发。文中涉及 wescode 的技术描述基于内部测试数据;涉及其他工具的描述基于各家公开文档和社区反馈。

模型的上下文窗口就像一个行李箱。问题不是"能装多少",而是"你装了什么进去"。

Claude 给你 200K token 的窗口,GPT-4o 给你 128K。听起来很大——但一个 5 万行的项目全部塞进去也只是 15 万 token 左右。再加上系统提示、工具描述、对话历史,窗口很快就满了。

满了怎么办?两种策略完全不同。


同一个任务,两种塞法

假设你有这样一个项目,要把 processPayment 的参数从 (amount: number) 改成 (amount: number, currency: string):

// src/gateways/PaymentGateway.ts
interface PaymentGateway {
  processPayment(amount: number): Promise<PaymentResult>
}

class StripeGateway implements PaymentGateway {
  async processPayment(amount: number): Promise<PaymentResult> {
    return await this.stripe.charges.create({ amount })
  }
}
// src/services/OrderService.ts
class OrderService {
  async checkout(input: CheckoutInput): Promise<Order> {
    const payment = await this.gateway.processPayment(input.amount)
    await this.auditLogger.log('payment_processed', payment)
    return { ...input, paymentId: payment.id }
  }
}

你让 AI 帮你"改 processPayment 的参数,加上 currency"。这时候 AI 需要知道:谁在调用这个函数?改动的影响范围是什么?

两种工具给 AI 看到的完全不同的东西。

Cursor 塞了什么(塞满策略)

Cursor 通过 Embedding 向量检索,按语义相似度找代码片段,从高到低往窗口里塞:

#塞进 prompt 的代码片段token 数是调用方吗
1PaymentGateway.ts — 接口定义 + StripeGateway 全文~2,800否(是定义)
2PaymentGateway.refundPayment()~1,500否(兄弟方法)
3PaymentGateway.validateCard()~1,200否(同类无关)
4PaymentHistory.getRecords()~2,100否(名字像)
5BillingService.calculateFee()~1,800否(语义近)
6PaymentWebhookHandler.ts — 全文~4,200⚠️ 有调用
7OrderService.checkout() — 所在文件全文~8,500✅ 真调用方
8PaymentNotifier.sendReceipt()~1,600否(名字像)
9test/payment.test.ts — 测试文件~6,800否(测试)
10StripeGateway.processPayment() 实现~2,000否(已在 #1)
11PaymentConfig.ts — 配置常量~900否(配置)
12types/payment.d.ts — 类型定义~1,400否(类型)
13SettlementJob.execute() — 所在文件全文~3,800✅ 真调用方
14PaymentLogger.ts — 全文~2,200否(日志)
15mock/paymentMock.ts~3,100否(mock)
16RefundService.processRefund()~2,400否(名字像)
17OrderController.createOrder() — 所在文件全文~5,200⚠️ 间接相关
18CurrencyConverter.ts — 全文~1,800否(语义近)
19PaymentErrorHandler.ts~2,600否(名字像)
20InvoiceService.generateInvoice()~3,200否(语义近)
合计~60,1002/20 真调用方

20 段代码吃掉 60K token,只有 2 个是真正要改的调用方。注意 Embedding 塞的是整个文件而不是函数——OrderService.checkout() 只有 6 行是关键的,但模型看到的是它所在的 500 行文件的全部 8,500 token。

上下文组装架构:CKG 驱动精确加载

wescode 塞了什么(塞对策略)

wescode 通过 CKG 调用图反向遍历 CALLS 边和 IMPLEMENTS 边,精确定位调用方,只放函数级代码:

#塞进 prompt 的代码token 数为什么在这里
1OrderService.checkout() — 6 行函数体~180CALLS 边:直接调用 processPayment
2SettlementJob.execute() — 12 行函数体~350CALLS 边 + IMPLEMENTS 解析:通过接口间接调用
3PaymentWebhookHandler.handle() — 8 行函数体~240CALLS 边:直接调用
4PaymentGateway 接口定义 — 4 行~120改动目标:接口签名需要同步改
七层记忆注入(项目约定 + 错误处理规范)~600认知结算产出的长期上下文
合计~1,4904/4 全部结构确认

加上系统提示和对话历史,总共约 8K token。

数字对比

塞满(Embedding 20 段)塞对(CKG 4 个函数)
input token~60,000~8,000
关键信息占比2/20 = 10%4/4 = 100%
干扰段数18 段0 段
API 费用(按 Claude input $3/M)$0.18$0.024
费用倍数7.5×1×
模型响应延迟慢(input 多,注意力分散)快

"塞对"的核心不是"少给",而是"给的全是对的"。 60K 的 prompt 里模型要自己从 20 段代码中筛出 2 个调用方——这和做阅读理解一样。8K 的 prompt 里 4 个函数全是经结构关系确认的调用方,模型直接分析改动影响,不需要做任何过滤。

塞满 vs 塞对:一次真实任务的全流程对比

在上面的 processPayment 加参数任务中,两种策略的端到端体验差距不仅在 token 数上:

维度塞满策略(Cursor)塞对策略(wescode)
检索耗时800-1200ms(向量查询 + 排序)15-50ms(SQLite 图查询)
input token~60,000~8,000
模型首 token 延迟3.2-5.1 秒0.8-1.6 秒
API 单次费用$0.18$0.024
返回结果准确率改了 2/3 个调用方(漏了 SettlementJob)改了 3/3 个调用方
需要人工检查的文件数20 个文件(确认哪些真的需要改)0 个(4 个全是确定的)
整轮耗时~12 秒~4 秒

为什么塞满策略漏了 SettlementJob?因为 SettlementJob 通过接口 PaymentGateway 间接调用 processPayment——它的代码里没有出现 processPayment 这个字符串,在 embedding 空间里和 processPayment 的语义距离远于 processRefund。CKG 通过 IMPLEMENTS 边知道 StripeGateway 实现了 PaymentGateway,再沿 CALLS 边找到所有调用 PaymentGateway.processPayment() 的节点,包括通过接口类型调用的 SettlementJob。

连续对话 10 轮的累计成本差距:

塞满策略:10 轮 × ~60K token = ~600K input token ≈ $1.80
塞对策略:10 轮 × ~8K token  = ~80K input token  ≈ $0.24

同一个重构任务,费用差了 7.5 倍。如果你每天和 AI 对话 30-50 轮,一个月下来差距是几十美元。


wescode 的三步上下文组装

wescode 不做向量检索,不按语义相似度排序。它的上下文组装分三步:

第一步:CKG 定位关系链

CKG(代码知识图谱)是 wescode 独有的索引结构。构建过程:tree-sitter 解析源码 AST → 10-Pass 管线提取符号和关系 → 写入本地 SQLite。六种关系边:

全部在本地完成,不上传代码。10-Pass 管线的关键步骤:

  1. 文件发现:扫描工作区,按语言分类(TypeScript / Python / Java / …)
  2. AST 解析:tree-sitter 将每个文件解析为语法树
  3. 符号提取:从 AST 中提取函数、类、接口、类型等符号及其行号范围
  4. 边构建:分析调用关系、继承关系、接口实现
  5. 跨文件解析:通过 import/export 将不同文件的符号关联起来
  6. 增量更新:只重建被修改文件的索引,平均响应 < 200ms

一个 10 万行的 TypeScript 项目首次索引约 5-15 秒,后续增量更新在文件保存时自动完成。

你问"改 processPayment 还影响谁",CKG 从改动点出发,沿三种边遍历:

processPayment (改动目标)
  ← CALLS ← OrderService.checkout()        // 直接调用方
  ← CALLS ← PaymentWebhookHandler.handle() // 直接调用方
  ← IMPLEMENTS ← StripeGateway             // 接口实现
    ← CALLS ← SettlementJob.execute()      // 通过接口间接调用

这是一次图查询,不是搜索。时间是毫秒级,结果是确定性的——同样的代码结构永远返回同样的 4 个节点。向量检索返回的是"语义距离最近的 K 个",每次可能不同,而且 RefundService.processRefund() 这种名字相近但调用关系无关的函数会排在前面。

CKG 的代价是构建时间:首次打开 10 万行项目约 5-15 秒。但之后保存文件触发增量更新,100-200ms 完成。

第二步:函数级加载

拿到 4 个节点后,wescode 只读取函数体,不是整个文件。

这件事的差距在大文件里尤其明显。假设 OrderService.ts 有 500 行,里面定义了 15 个方法:

策略加载内容token 数
Cursor(整个文件)OrderService.ts 全部 500 行~8,500
wescode(函数级)checkout() 的 6 行函数体 + 签名~180

同一个信息源,token 差了 47 倍。乘以 4 个调用方,差距是数量级的。

函数级加载依赖 CKG 的 DEFINES 关系——每个符号在哪个文件的第几行到第几行,索引时已经记录。读文件时精确 seek 到那个范围,不需要把整个文件都读进内存。

CKG 调用图理解示意

用一个 Java 的例子来进一步说明函数级加载的价值。假设你有一个典型的 Spring Service 类:

// src/main/java/com/example/service/PaymentService.java (600 行)
@Service
public class PaymentService {
    // ... 构造器、依赖注入等 (约 30 行)

    public PaymentResult processPayment(BigDecimal amount) { /* 你要改的方法 - 20 行 */ }
    public RefundResult processRefund(String paymentId) { /* 无关 - 35 行 */ }
    public void validateCard(CardInfo card) { /* 无关 - 25 行 */ }
    public List<Payment> getHistory(String userId) { /* 无关 - 40 行 */ }
    public PaymentStats calculateStats() { /* 无关 - 50 行 */ }
    // ... 还有 10 个方法,总共 600 行
}

塞满策略会加载整个 600 行文件(约 10,200 token)。塞对策略只加载 processPayment 的 20 行函数体(约 340 token)。差距是 30 倍——而且那 580 行"额外"内容不仅浪费 token,还可能误导模型去改不该改的方法。

第三步:记忆补充长期上下文

项目的基础信息——"用 TypeScript + PostgreSQL""错误抛 AppError 不抛原生 Error""金额统一用分为单位"——这些东西每次对话都需要,但不应该每次都占窗口。

wescode 的七层记忆系统解决这个问题:

记忆层存什么怎么来的
环境操作系统、Node 版本、项目依赖首次打开项目自动探测
共识团队编码约定、架构决策admin 写入(团队场景)
关于我用户的个人偏好和工作习惯对话中学到
角色记忆每个 Agent 的专业知识对话中积累
会话当前对话的关键认知认知结算自动提炼
工作当前任务的临时状态运行时产出
未知无法分类的条目兜底

认知结算(Cognitive Settlement)是关键机制:每次对话结束时,主对话模型自动把这轮对话的关键认知提炼成摘要,存入长期记忆。下次对话不需要从头解释"我们项目用 TypeScript"——AI 已经记住了。

这些记忆按项目物理隔离(1 workspace = 1 Cell),不会串到别的项目里。

一个 Python 项目的具体例子:你第一次告诉 AI"我们项目用 Pydantic v2 的 model_validator 不用 v1 的 @validator":

# AI 第一次对话时学到这个偏好
from pydantic import BaseModel, model_validator

class OrderRequest(BaseModel):
    amount: float
    currency: str

    @model_validator(mode='after')
    def validate_amount(self) -> 'OrderRequest':
        if self.amount <= 0:
            raise ValueError('Amount must be positive')
        return self

认知结算把"用 Pydantic v2 API"存进"关于我"记忆层。三天后你开新对话改另一个 Model,AI 直接用 model_validator 而不是旧版 @validator。这 600 token 的记忆注入替代了本该在系统提示里占 2000+ token 的项目背景说明。


长对话中的差距

两种策略在 1-3 轮的短对话中差异不大。差距在长对话中暴露:

第 N 轮塞满策略塞对策略
第 5 轮窗口还富裕,加载顺畅同
第 15 轮窗口开始紧张,旧上下文被丢弃窗口仍然富裕(每次只放需要的)
第 25 轮早期对话的关键信息已被丢弃,模型开始"忘事"历史关键信息在记忆系统里,不占窗口
第 30 轮提示"上下文即将满"或质量明显下降仍然流畅

背后的算术:塞满策略每轮消耗 50-80K token 的窗口空间,30 轮对话的历史就超过 200K。即使模型窗口有 200K,也只能保留最近几轮——早期的关键决策、修改原因、架构讨论都被丢掉了。

塞对策略每轮消耗 5-15K token,30 轮对话的历史约 30-50K,剩余窗口仍然富裕。再加上认知结算把历史摘要存入记忆层,即使窗口丢弃了旧消息,关键信息仍然可以从记忆中召回。

一个真实的长对话场景:你正在做一个跨 8 个文件的重构任务,前 5 轮你和 AI 讨论了架构方案并确定了"保持向后兼容、旧接口标记 deprecated 但不删除"的策略。到第 20 轮,塞满策略的窗口已经把第 5 轮的对话丢弃了——AI 生成的代码直接删掉了旧接口,完全忘了之前的"保留 deprecated"决策。塞对策略在第 5 轮的认知结算中把这条决策存进了记忆,第 20 轮仍然遵守。

Before/After 对比:上下文策略的实际效果


各工具的上下文策略对比

工具主策略怎么选代码加载粒度长期记忆代码是否上传
Cursor塞满Embedding 向量相似度排序文件级或大段落无上传算向量
Copilot塞满打开的文件 + 语义搜索文件级无上传
Claude Code混合grep 结果 + 模型自己选读文件文件级无独立记忆通过 API 传输
通义灵码塞满当前文件 + 跨文件 Embedding文件级无上传
Trae塞满当前文件 + Embedding文件级无上传
wescode塞对CKG 调用图遍历函数级七层记忆 + 认知结算不上传(BYOK 直连)

为什么"混合"不等于"塞对"

有人会说:Cursor 也有语义搜索,Windsurf 也有索引,为什么不算"塞对"?

区别在于组装的决策依据。向量检索回答的是"语义最近的 K 段代码"——processRefund 和 processPayment 名字相似、语义接近,在 embedding 空间里距离很小,但你改 processPayment 的参数时 processRefund 完全不需要出现在上下文里。CKG 回答的是"结构上调用了它的、实现了它的、导入了它的具体节点"——这是确定性的图查询,不依赖语义距离。

同理,Windsurf 的 Cascade 标注"indexed"但未公开索引类型。如果底层仍是 embedding 检索加上打开过的文件列表,那它的"混合"本质上仍是塞满策略的优化版,而不是塞对。

Claude Code 是个有趣的中间状态——它没有预建索引,但让模型自己决定"我还要看哪个文件"。这比盲目塞满好,但每次"看文件"都是一次工具调用(又一轮 API 请求),成本和延迟叠加。更关键的是它读到的仍然是整个文件,不是函数级片段。


"塞对"做不到什么

诚实说,精确上下文有代价:

场景塞对策略的表现为什么
"找一段类似的排序逻辑"不擅长CKG 找的是调用关系,不是语义相似
首次打开项目(索引中)和其他工具差不多CKG 正在构建,10 万行约 5-15 秒
动态派发(obj[methodName]())调用链断裂运行时才能确定目标,静态分析失效
跨语言边界(REST/gRPC 除外)调用链断裂TypeScript 调 WebAssembly,tree-sitter 跨不过去

"找类似代码"的场景确实是 Embedding 的强项。wescode 选择不做 Embedding——放弃了语义搜索的便利性,换来了影响分析的精确性。这个取舍在以修改为主的日常编码中是值得的:你每天的操作 80% 是"改代码",不是"找类似代码"。

如果你确实需要语义搜索——比如"找一段类似的排序算法"——wescode 有 search_files 工具做全文搜索,也可以让 AI 用 grep 工具在终端里找。它们不如 embedding 搜索在语义相似度上精准,但对"找包含某关键词的代码"场景够用了。

综合能力对比矩阵


实例:Python FastAPI 项目的上下文装配

用一个 Python 后端项目说明"塞对"在非 TypeScript 语言中怎么工作。假设你在改 services/order.py 的 create_order 函数:

# services/order.py
from repositories.order_repo import OrderRepository
from services.inventory import InventoryService
from services.notification import NotificationService

class OrderService:
    def __init__(self, repo: OrderRepository, inventory: InventoryService, notify: NotificationService):
        self.repo = repo
        self.inventory = inventory
        self.notify = notify

    async def create_order(self, user_id: str, items: list[dict]) -> dict:
        # 检查库存
        for item in items:
            available = await self.inventory.check_stock(item["sku"], item["quantity"])
            if not available:
                raise ValueError(f"库存不足: {item['sku']}")
        # 创建订单
        order = await self.repo.insert(user_id=user_id, items=items, status="pending")
        # 发送通知
        await self.notify.send_order_confirmation(user_id, order["id"])
        return order

当你问 AI"给 create_order 加一个优惠券参数":

塞满策略会通过 embedding 找到语义相近的文件——可能包括 services/refund.py(名字里也有 order)、tests/test_order.py(测试文件)、models/order_model.py(数据模型),凑满 60K token。

塞对策略会通过 CKG 精确定位:

create_order
├── CALLS → OrderRepository.insert        (需要改参数)
├── CALLS → InventoryService.check_stock   (不需要改)
├── CALLS → NotificationService.send_order_confirmation (不需要改)
└── CALLED_BY → routes/order_routes.py::create_order_endpoint (需要改路由参数)
    └── CALLED_BY → tests/test_order_routes.py::test_create_order (需要改测试)

最终上下文:5 个函数体,约 3K token。模型知道要改哪几处,也知道 check_stock 和 send_order_confirmation 不需要改——这个"不需要改"的判断同样重要。


长对话成本累积

单轮差 5 倍,10 轮累积差多少?以一个 10 轮连续重构对话为例("改 create_order 加优惠券 → 改测试 → 改文档 → 修 bug → ..."):

轮次塞满策略(累积 token)塞对策略(累积 token)
第 1 轮60K8K
第 3 轮180K(含历史消息)30K
第 5 轮300K(可能已触发压缩)55K
第 10 轮500K+(多次压缩,信息丢失)100K(记忆系统补充历史)

10 轮下来,塞满策略的 API 成本约 5-8 美元(GPT-4o 价格),塞对策略约 0.8-1.5 美元。更关键的是,塞满策略在第 5 轮左右就触发了上下文压缩,模型开始"忘记"前几轮的约定;塞对策略通过七层记忆系统保留跨轮次的关键信息,第 10 轮仍然记得第 1 轮你说的"优惠券只支持满减类型"。


常见问题

Q1:模型越来越大,以后窗口不够用的问题会不会自然消失? 不会完全消失。两个理由:(1) 窗口再大,成本和延迟仍然与 token 数正比——200K 窗口塞满的费用是 20K 的 10 倍;(2) 研究表明模型对长上下文中间部分的注意力会下降("Lost in the Middle" 效应),塞得越多不代表理解得越好。实际影响:一个 128K 窗口塞 60K 上下文的请求,延迟约 3-5 秒(取决于模型),同样的任务用 8K 上下文只需 0.8-1.5 秒。

Q2:Cursor 的上下文管理在改进,以后会追上吗? Cursor 确实一直在迭代。但"塞满"和"塞对"的差距不是工程优化能弥合的——它是信息来源的区别。Embedding 衡量的是语义距离,CKG 走的是调用关系,两者回答的是不同的问题。只要你的任务是"改代码、查影响",调用关系就比语义距离更精确。Cursor 也可以做调用图索引,但这需要接入语言级解析器(tree-sitter 或 LSP),重新设计整个检索管线——不是一个季度的迭代能完成的。

Q3:CKG 构建需要时间,第一次打开项目时上下文质量也不好吧? 是的。首次打开项目时 CKG 在建索引(10 万行约 5-15 秒),在索引完成前上下文质量和其他工具差不多。索引完成后状态栏会显示"AI 就绪",之后才进入"塞对"模式。保存文件后增量更新在 100-200ms 内完成。实际体验是:打开项目等几秒钟,之后每次交互都是精确上下文。

Q4:记忆会不会记错东西、串到别的项目? 记忆按项目物理隔离——wescode 的架构是 1 workspace = 1 Cell,每个项目的记忆、索引、知识库是独立的数据库文件。切换项目 = 切换 Cell = 整个记忆面一起换。记忆内容有威胁扫描和质量门控,凭据类内容会被拦截。如果 AI 记错了某个偏好,你可以在记忆中心里直接查看和删除。

Q5:8K token 的上下文遇到特别复杂的重构,够用吗? 对于涉及 10+ 个文件的大规模重构,8K token 的单轮上下文确实可能不够。但 wescode 的做法是拆轮次而不是扩窗口——Plan 工具把大任务分解成步骤,每步精确加载那一步需要的上下文。30 步的重构,每步 8K,总共消耗 240K token,但每一步的上下文都是精确的。相比之下,塞满策略的单步可能用掉 60K token 但其中 80% 是噪音。


本文对各工具上下文策略的描述基于其 2026-09-20 的公开行为。如有更新,以各家最新页面为准。