Agent 模式的三条技术路线,各自的天花板

wescode · 2026-11-10 · Agent / 技术路线 / 对比

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

你让 AI 把一个 Express REST API 迁移到 Fastify。它改了路由定义,跑起来了。你以为完成了。

三天后线上报了一个诡异的 bug:authMiddleware 还是 Express 的,挂在一个 Fastify 路由上——不报错,但 req.user 永远是 undefined。AI 改了最显眼的 10 个文件,漏了第 11 个。它不知道那个中间件存在,因为没有任何东西告诉它 authMiddleware 和 userRoutes 之间有调用关系。

这不是模型不够聪明。这是架构决定的天花板。

2026 年的 AI 编程 Agent 实现,按架构可以归为三条路线。我们用同一个任务——把 Express API 迁移到 Fastify(约 8000 行,12 个路由文件,6 个中间件,40+ 个测试)——展示各自怎么做、做到哪就做不动了。


路线一:提示词路由

根据意图选模板、调模型、执行工具、返回结果。每一步相对独立,步骤间上下文断裂。

// 路线一:提示词路由的核心循环
async function promptRouting(task: string) {
  const intent = classifyIntent(task)            // "迁移框架"
  const template = selectPrompt(intent)          // 选预设模板
  const context = gatherCurrentFile()            // 只有当前打开的文件

  const plan = await llm.generate(template, context)

  for (const step of plan.steps) {
    const result = await executeTool(step)        // 每步独立调用
    // ⚠️ 步骤 2 不知道步骤 1 改了什么
    // ⚠️ 上下文 = 模板 + 工具返回值,没有全局视图
    if (result.error) {
      return { error: result.error }              // 报错就停,无恢复机制
    }
  }

  return plan.summary
  // Run 结束:无认知产物,下次从零开始
}

Express→Fastify 实际执行:

Agent 收到"迁移 Express 到 Fastify"的指令,匹配到"框架迁移"模板。模板告诉它:找到入口文件、修改路由定义、更新依赖。它打开 app.ts,改了路由注册方式;接着打开 routes/users.ts、routes/orders.ts,改了处理函数签名(3 步)。然后 Agent 认为任务完成——因为模板没有提示"检查中间件"。

authMiddleware 仍然是 Express 的 (req, res, next) 签名。你手动指出"还有中间件",它改了 authMiddleware,又漏了 errorHandler、rateLimiter。每次你发现一个遗漏,就要开一轮新对话重新描述上下文。3-5 步后需要人工介入,每个遗漏又是一轮。

问题的根源不是模型"不够聪明",而是架构上不存在"发现未知影响面"的机制。模型能看到的 = 你告诉它的 + 模板预定义的。模板没写"检查 config/"下面的文件,Agent 就不会去看——即使 config/server.ts 里有一行 express.static() 正等着爆炸。

天花板在哪:Agent 的视野 = 你告诉它的 + 模板预定义的。它不会自己去"发现"代码中的依赖关系。


路线二:上下文编排

有上下文窗口管理,能在多步操作中维持连贯的执行环境。核心能力是从代码库中智能选取相关文件填入上下文窗口。

Agent 核心循环的架构

// 路线二:上下文编排的核心循环
async function contextOrchestration(task: string, codebase: Index) {
  const relevant = await codebase.semanticSearch(task)  // Embedding 检索相关文件
  let context = buildWindow(relevant, MAX_TOKENS)       // 上下文窗口管理
  let iteration = 0

  while (!done && iteration < MAX_ITERATIONS) {
    const action = await llm.decide(context, tools)     // 模型决策下一步
    const result = await executeTool(action)
    context = rebalanceWindow(context, result)           // 窗口满了截断旧内容

    if (result.error) {
      context = injectError(context, result.error)       // 错误信息留在上下文里
      // ✅ 能在上下文内修正错误
    }
    iteration++
    // ⚠️ 关掉对话 → 所有"理解"消失
    // ⚠️ 没有长期记忆,不知道你上周说过"统一用 async/await"
  }
}

Express→Fastify 实际执行:

Embedding 索引找到了路由文件(语义上"Express route"和"Fastify route"很近)、大部分中间件(名字里有"middleware")、部分测试。Agent 开始连续执行:改 app.ts、改 routes/*.ts、改 middleware/auth.ts、改 middleware/cors.ts、改 middleware/logger.ts——十几步,改了 10 个文件。

但漏了两处:config/server.ts 里的 express.static() 调用——文件名是"server config"而非"Express middleware",语义搜索排名靠后;test/integration/auth.test.ts 里的测试 helper——测试文件太多,语义排名分散,窗口截断了集成测试部分。

这不是 Embedding 模型的质量问题——换一个更好的向量模型不会从根本上解决。Embedding 衡量的是语义相似度("这段文字和搜索词有多像"),而代码依赖是结构关系("这个函数调用了那个函数")。config/server.ts 和"Express 迁移"在语义上就是不相关的——但在代码结构上是强关联的。

另一个问题出现在窗口截断时:上下文满了,Agent 丢掉了最早放入的文件内容,包括 app.ts 的初始结构。到第 12 步时,Agent 已经"忘了"入口文件长什么样,做出的修改可能和第 1 步的修改产生冲突而不自知。

用 Python 展示 Embedding 搜索和 CKG 调用图的本质差异:

# Embedding 语义搜索 vs CKG 调用图——本质差异

# --- Embedding 搜索 ---
# 问题:找到和 "Express 迁移" 相关的文件
query_embedding = model.encode("Express Fastify migration middleware")
results = vector_db.search(query_embedding, top_k=20)
# results:
#   routes/users.ts     → 0.92(文件名和内容都含 "route")
#   routes/orders.ts    → 0.91
#   middleware/auth.ts   → 0.85(名字含 "middleware")
#   middleware/cors.ts   → 0.83
#   app.ts              → 0.80
#   ...
#   config/server.ts    → 0.31  ← 语义距离远,被截断
#   test/helpers/mock.ts → 0.28  ← 排名靠后,不进窗口

# --- CKG 调用图 ---
# 问题:找到和 express 模块有调用关系的所有文件
callers = ckg.query("""
    SELECT DISTINCT source_file 
    FROM edges 
    WHERE target_symbol LIKE 'express.%'
       OR target_module = 'express'
""")
# callers:
#   app.ts              → IMPORTS express
#   routes/users.ts     → CALLS express.Router()
#   routes/orders.ts    → CALLS express.Router()
#   middleware/auth.ts   → CALLS (req, res, next) 签名
#   middleware/cors.ts   → CALLS (req, res, next) 签名
#   config/server.ts    → CALLS express.static()  ← 被精确找到
#   test/helpers/mock.ts → IMPORTS express for mock ← 被精确找到

关键区别:Embedding 是"名字像不像"的概率匹配,CKG 是"谁真的调用了谁"的确定性追溯。8000 行项目里两者的差异可能是 2 个文件(如上例);80000 行项目里差异可能是 20 个文件——后者就是线上 bug 的来源。

天花板在哪:Embedding 找的是"名字像的代码"而不是"有调用关系的代码"。config/server.ts 的文件名和 Express 没有语义关联,但它引用了 express.static()——这个关系只有调用图能捕获。


路线三:认知引擎

有持久记忆、Plan 系统、循环检测、认知结算。不只执行任务,还积累经验。

// 路线三:认知引擎的核心循环(wescode 的 Agent Loop)
async function cognitiveEngine(task: string, cell: Cell) {
  const memory = await cell.memory.recall(task)          // 注入长期记忆
  const context = assembleContext(memory, task)           // 加载记忆 + 项目上下文
  const plan = await llm.createPlan(context)             // 线性 Plan 清单

  while (plan.hasPendingSteps()) {
    const step = plan.nextStep()
    const impact = await cell.ckg.analyzeImpact(step)    // CKG 精确影响面
    const result = await executeTool(step, impact)

    await cell.cse.checkConstraints(result)              // CSE 约束检查
    await cell.verify(result)                            // L2.5 行为基线验证

    if (cell.cycleDetector.isLoop(result)) {             // 循环检测
      const healed = await cell.cycleDetector.selfHeal() // 首次:注入纠正消息+重置
      if (!healed) await cell.hitl.askUser()              // 第二次:HITL 问用户
    }
    if (context.nearWindowLimit()) {
      await midRunCompress(context)                      // MidRunCompression
    }
    plan.markDone(step)                                  // 引擎盖章(工作账本)
  }

  await cell.cognitiveSettlement()                       // 认知结算:主模型提炼→持久化
}

Express→Fastify 实际执行:

CKG 调用图直接告诉 Agent:authMiddleware 被 userRoutes、orderRoutes 等 5 个文件 CALLS;errorHandler 被 3 个路由文件引用;rateLimiter 被 app.ts IMPORTS;config/server.ts 调用了 express.static()。Plan 列出 14 步,覆盖全部有关系的文件。

第 7 步 CSE 发现 errorHandler 仍用 (err, req, res, next) 四参数签名(Express 约定),标记为约束违反,Agent 立即修正为 Fastify 的 (error, request, reply) 签名。第 12 步 L2.5 行为基线跑通集成测试并对比迁移前后的 API 响应——确认 GET /users/:id 的响应结构和状态码与迁移前一致,同时检测到 POST /orders 的错误返回格式从 { error: "..." } 变成了 { message: "..." },标记为行为变化,Agent 修正了错误处理函数的返回格式。

第 9 步时 Agent 改 rateLimiter 后 build 失败——fastify-rate-limit 的配置格式和 express-rate-limit 不同。Agent 在上下文内看到错误,调整了配置格式。如果同一个错误连续出现(完全相同的操作 + 完全相同的结果),循环检测触发:首次注入纠正消息让模型换一个方向尝试;第二次触发 HITL 问你"这个 rate-limit 配置应该怎么写"。

与 Claude Code 的 Agent 方式对比

Run 结束后认知结算把本次迁移的关键发现写入长期记忆——"Express→Fastify 中间件签名差异:(req, res, next) → (request, reply)"、"fastify-rate-limit 配置格式与 express-rate-limit 不兼容"、"错误返回格式需统一为 { error: ... } 而非 { message: ... }"。下次类似迁移直接知道这些要点,不需要重新踩坑。这些产物不是"保存对话历史"——是主模型提炼出的结构化认知,写入 Memory 后自动注入未来的对话上下文。

天花板在哪:依赖 CKG 调用图的完整性。动态分派(obj[methodName]())CKG 无法静态追踪;跨仓库依赖不在索引范围内。


Express→Fastify 迁移的逐步对比

同一个 Express 项目(约 8000 行,12 个路由文件,6 个中间件,40+ 个测试):

步骤提示词路由上下文编排认知引擎(wescode)
影响面定位靠 Prompt 描述,Agent 只看到你提到的文件Embedding 语义搜索,按"名字像不像"排名CKG 调用图遍历,6 种关系边精确定位
改路由定义✅ 改了 3 个主要文件✅ 改了 10 个文件✅ 改了 12 个文件(CKG 给出完整清单)
改中间件签名未检测到中间件⚠️ 改了 4/6(2 个语义距离远被漏掉)✅ 改了 6/6(CALLS 关系直接命中)
改配置文件不在视野内⚠️ config/server.ts 语义排名靠后被漏✅ CSE 检查到仍引用 express.static
更新测试未涉及测试⚠️ 部分(窗口截断漏掉集成测试)✅ CKG 追溯测试中对路由 helper 的调用
行为验证无可运行测试但不对比行为差异L2.5 对比迁移前后 API 响应
循环检测报错后重试或终止有限重试相同操作+结果 → 自愈 → HITL
下次同类任务从零开始从零开始Memory 已有相关经验
人工介入次数3-5 次1-2 次0 次(CKG 覆盖完整时)

Before/After 对比——同一个重构任务的完成过程:

维度Before(路线一/二)After(路线三)
定位影响面人工列清单 or Embedding 猜测,~70% 覆盖(内部测试数据)CKG 调用图自动遍历,~95% 覆盖(内部测试数据)
发现第一个遗漏的时间上线后(线上 bug 报告)执行中(CKG 在 Plan 阶段就列出)
修复遗漏的成本新对话 + 重新描述上下文 + 手动指出文件Agent 自行在当前 Plan 内处理
同类迁移的第二次时间和遗漏率与第一次相同Memory 已有经验,遗漏率下降
一周内完成 3 次类似迁移的总时间3 × 2 小时 = 6 小时2 + 1.5 + 1 = 4.5 小时(认知积累)

Agent Loop 架构全貌

wescode 的 Agent Loop 不是一个简单的 while 循环。完整流程:

用户请求
  → 加载长期记忆(偏好 / 纠正 / 经验)+ 组装上下文
  → Plan 规划(线性清单,模型驱动)
  → 执行工具调用
    → CKG 影响面分析(调用图精确定位谁受影响)
    → 验证(构建 / 测试 / CSE 约束检查 / L2.5 行为基线)
    → 循环检测(完全相同操作+结果 → 首次自愈,第二次 HITL)
    → MidRunCompression(上下文接近窗口上限时自动压缩)
  → 步骤完成 or 继续循环
  → 所有步骤完成 → Run 结束
    → 认知结算(主模型提炼认知产物 → 持久化记忆)

几个关键设计决策:

Plan 是线性清单,不是 DAG。 模型驱动步骤完成(plan(action=update, status=done)),引擎只做工作账本盖章——记录"这步是否有非 plan 工具调用发生过"。引擎不隐式推进、不自动跳过、不做认知判断。为什么不用 DAG?因为 LLM 擅长线性推理,DAG 依赖声明(depends_on)引入了"引擎决定步骤先后"的复杂性,实测中 DAG 反而让模型更容易困惑。

循环检测的两阶段终止协议。 CycleDetector 检测的是"可证明的死循环"——完全相同的操作 + 完全相同的结果,连续出现多次。不是"你跑太久了"的计时器。第一次触发时注入纠正消息并重置计数器;第二次触发才 HITL 问用户。超时 60 秒无响应则自动终止并保存进度。

用 Java 伪代码理解循环检测的两阶段协议:

// 循环检测的两阶段终止协议——概念模型
public class CycleDetector {
    private int identicalCallCount = 0;
    private String lastCallSignature = "";  // 操作 + 参数 + 结果的哈希
    
    // 阈值配置(wescode 编程场景的默认值)
    static final int IDENTICAL_WARN = 4;   // 警告阈值
    static final int IDENTICAL_TERM = 7;   // 终止阈值
    static final int ACTION_TERM = 50;     // 同类工具累计阈值
    
    public CycleResult check(ToolCall call, ToolResult result) {
        String signature = hash(call.name, call.args, result.output);
        
        if (signature.equals(lastCallSignature)) {
            identicalCallCount++;
        } else {
            // 参数或结果不同 → streak 减半(progress-aware)
            identicalCallCount = Math.max(0, identicalCallCount / 2);
            lastCallSignature = signature;
        }
        
        if (identicalCallCount >= IDENTICAL_TERM) {
            // 第二阶段:HITL 问用户
            return CycleResult.HITL_REQUIRED;
        } else if (identicalCallCount >= IDENTICAL_WARN) {
            // 第一阶段:注入纠正消息,给模型第二次机会
            return CycleResult.SELF_HEAL;
        }
        return CycleResult.OK;
    }
}

认知结算是 Run 生命周期的强制阶段。 每次 Run 结束时主对话模型提炼四类认知产物(会话状态 / 事实偏好 / 纠正 / 教训),写入持久化 Memory。认知结算用的是主对话模型本身,不是辅助模型——"总结质量 = 对话质量"。

Run 生命周期与 SSE 连接解耦。 网络断开不会取消正在执行的 Run——重新连接后可以续播事件。


各路线的天花板

Code Review 场景中三条路线的差异

维度提示词路由上下文编排认知引擎
最大自主步数3-5 步(上下文断裂)15-30 步(受窗口限制)50+ 步(MidRunCompression + 循环检测)
错误恢复报错后重试或终止上下文内修正,但不记得犯过同样的错两阶段终止 + 认知结算避免重蹈覆辙
跨会话连续性无——关掉对话一切归零有限(需手动记忆保存)引擎级持久化 Memory,自动注入
行为验证无可运行测试,不对比行为差异L2.5 行为基线——"测试绿了但行为变了"也能检测
代码结构理解依赖模型自身能力Embedding 语义搜索CKG 调用图(10 万行 5-15 秒,内部测试数据)
适合的任务规模单文件修改、简单 bug fix跨文件功能开发、中等重构大型重构、持续迭代、需要跨会话一致性
认知积累无无认知结算提炼关键发现,复利增长

CKG + CSE + L2.5 各做什么

三个组件覆盖"找到影响面 → 确认改法正确 → 验证改完没问题"的完整链路:

组件回答的问题原理性能
CKG(代码知识图谱)"改了这里还要改哪里"tree-sitter 本地构建调用图,6 种关系边10 万行 5-15 秒,增量 100-500ms(内部测试数据)
CSE(约束满足引擎)"这个改动违反了项目的隐式规矩吗"签名约定、分层规则、命名规范、依赖约束实时检查,每次工具调用后触发
L2.5(行为基线)"测试绿了,但行为变了吗"对比修改前后 API 响应结构、状态码、错误消息取决于测试套件规模

CKG 告诉你影响面(改哪里),CSE 告诉你约束(怎么改才对),L2.5 告诉你结果(改完真的没问题吗)。三者的结合是路线三与路线二的核心差异。


wescode 的 Agent 做不到什么

诚实说:

做不到为什么替代方案
跨仓库协调1 workspace = 1 Cell,物理隔离分别在各仓库执行,人工协调接口契约
商业判断决策API 设计取舍、产品优先级不是代码分析能回答的Agent 提供技术对比,人做最终决策
UI / E2E 自动化无内置浏览器自动化Playwright MCP(可选外设)
动态分派追踪obj[methodName]() 运行时才确定grep 兜底 + 类型推断,覆盖率约 85-95%
超大规模重构(100+ 文件)上下文压力和验证复杂度同时增长分批执行,每批 20-30 文件
你没表达过的偏好结算只存你明确表达过的事实和偏好显式告诉 Agent 或写入规则文件

FAQ

Q:路线二已经够用了,为什么还要路线三?

对"在同一个对话里完成一个功能"的场景,路线二确实够用——Cursor 在这个赛道上是标杆。问题在两方面:一是跨会话连续性——你上周教 AI 的项目约定,这周换个对话就全忘了;二是结构分析的准确性——Embedding 找"名字像的代码",CKG 找"真正有调用关系的代码"。8000 行项目差异不大,80000 行项目会明显感知到。

Q:认知引擎是不是过度设计?

对 10 分钟完成的单文件修改,确实体现不出优势。投入产出比在大型项目持续开发时才显现:跨会话认知连续性、CKG 精确影响分析、L2.5 行为验证——这些在长期项目中的收益远超初期学习成本。这和数据库选型一样——SQLite 够用就不要上 PostgreSQL,但数据量和并发上来了你需要它。

Q:三条路线会融合吗?

大概率会。上下文编排型已在吸收记忆特性(如 Cursor Memory),认知引擎型也在优化开箱体验。最终差异不在"有没有某个功能",而在各自的工程深度——就像数据库都支持 SQL,但底层存储引擎的差异决定了各自擅长的场景。

wescode 的编程助手全景

Q:选哪条路线取决于什么?

取决于你的任务特征。单文件改 bug → 路线一或路线二都够。跨文件功能开发 → 路线二。大型重构 + 长期迭代 + 需要跨会话一致性 → 路线三。不存在"最好的路线",只有最匹配当前任务规模和周期的路线。

Q:CKG 索引 10 万行要 5-15 秒,大项目会不会很慢?

首次索引是全量扫描,之后文件变更触发增量更新,通常在 100-500ms 完成。索引跑在本地 SQLite 上,不需要联网。对比 Embedding 方案:同规模项目的首次向量化通常需要几分钟且需要向量数据库。CKG 更快、更精确、完全离线。

Q:用了路线三就不需要人工 code review 了吗?

不是。Agent 做的是"已知规则的自动化验证"——CSE 检查签名约定、L2.5 对比行为差异。但 code review 的核心价值在"设计思路对不对"、"抽象合理吗"——这些需要项目上下文和工程品味,Agent 做不到。正确的分工:Agent 消灭机械性遗漏,人类聚焦设计判断。

Q:认知结算会拖慢每次对话吗?

结算发生在 Run 结束后,不阻塞你的下一条消息。耗时通常在 2-5 秒。结算产物对用户不可见——不会弹出气泡,你唯一感知到它的方式是"下次对话时 Agent 记得上次的项目细节"。


本文对各路线能力的描述基于 2026-09 的公开文档和功能。


三条路线不是好坏之分,是天花板不同。如果你的日常主要是单文件修改和快速问答,路线一已经够用;如果经常做跨文件重构和长期迭代,值得试试引擎级的认知循环。