写在前面
说实话,第一次用官方 Claude Code 的时候,我是真兴奋。终端里跑着 AI,能帮我改代码、跑命令,感觉像有了个 24 小时在线的高级工程师搭档。
但用了两周后,我开始纠结。
为什么每次切换模型都要重启会话?
为什么一个模型要包揽所有活儿,明明有些任务它并不擅长?
为什么补全系统这么笨,输个dq都匹配不到dao-qi?
这些问题像苍蝇一样在我脑子里嗡嗡响。直到某天晚上,我盯着终端里那个"请重启会话以切换模型"的提示,突然一拍桌子——不行,我得自己搞一个。
这就是 Kode 诞生的故事。今天我想聊聊,在重构这个 AI 助手的过程中,我们踩过的坑、做出的取舍,以及那些让我半夜三点还在兴奋地敲键盘的设计决策。
一、多模型协作:从"单打独斗"到"团队作战"
1.1 问题的本质
官方 Claude Code 的设计很简单:一个会话绑定一个模型。这听起来没问题,直到你真实使用:
你想让o3帮你设计系统架构(它的推理能力是真的强)
然后让Opus帮你写具体代码(它对工程细节的把握更扎实)
最后用Haiku快速处理一些简单任务(省成本啊!)
但在官方实现里,这三个需求要开三个会话。你的上下文、你的思考、你之前的讨论……全部要重来一遍。
这就像你明明有个全能团队,却每次只能派一个人上场。
1.2 我们的方案:模型指针系统
我们设计了一个简单的概念:模型指针。
{ "modelPointers": { "main": "claude-sonnet-4-5-20250929", // 主对话用哪个模型 "task": "claude-opus-4-5-20251101", // Subagent 用哪个模型 "reasoning": "o3", // 复杂推理用哪个模型 "quick": "claude-haiku-4-5-20251001" // 快速任务用哪个模型 } }这四个指针分别对应不同的使用场景。当你启动一个 Subagent 时,它会自动使用task指针指向的模型,而不用你每次手动指定。
但这只是开始。
1.3 真正的突破:运行时切换
我印象最深的是实现运行时模型切换的那天。
我们不想让用户重启会话。但这有个技术难点:AI 模型的上下文窗口大小不一样,API 格式也不一样,甚至有些模型根本不支持某些功能。
怎么在保持对话连续性的前提下切换模型?
我们的解决方案分三步:
上下文自适应裁剪:每个模型都有自己的
maxTokens配置,切换时自动计算需要裁剪多少历史消息工具兼容性检查:某些工具可能只在特定模型上可用,切换前要做验证
成本追踪分离:每个模型的 token 消耗独立统计,用户可以清楚地看到"这个任务用 o3 花了
0.02"
代码大概长这样(简化版):
// src/utils/model/modelManager.ts (简化示意) class ModelManager { private profiles: Map<string, ModelProfile> private pointers: Record<string, string> resolveModel(pointer: string): ModelProfile { const profileName = this.pointers[pointer] || this.pointers.main const profile = this.profiles.get(profileName) if (!profile) { throw new Error(`Model ${profileName} not configured`) } return profile } // 关键:获取模型的上下文窗口大小 getMaxTokens(modelName: string): number { const profile = this.profiles.get(modelName) return profile?.maxTokens || 200000 // 默认 200k } }这个设计带来的效果是啥?
你可以这样使用:
# 主对话用 Sonnet "帮我设计一个消息队列系统的架构" # Tab 键切换到 o3,继续对话 "用 o3 的深度推理能力,帮我分析这个架构的瓶颈" # 再切换到 Qwen Coder "现在帮我实现这个架构的代码"整个过程中,上下文一直在,思考一直在,只有模型在变。
1.4 专家咨询:AskExpertModel 的设计哲学
多模型协作的另一个场景是:主模型遇到难题,需要"请教"专家。
比如你正在用 Sonnet 写代码,突然遇到一个复杂的算法问题。Sonnet 可能不太擅长,但你不想切换整个会话到 o3(那样会打断你的思路)。
我们的解决方案是AskExpertModel工具:
// 用户输入(在 Sonnet 会话中) @ask-o3 这个算法的时间复杂度怎么分析?我有点卡住了 # AI 内部调用 { "tool": "AskExpertModel", "input": { "question": "完整的问题描述+上下文...", "expert_model": "o3", "chat_session_id": "new" // 可以继续之前的专家会话 } }这里的坑是什么?
专家模型是"孤立"的——它看不到你当前的对话上下文,除非你显式地传给它。
所以我们设计了一个强制要求:question参数必须是完全自包含的。AI 被训练成必须在问题中包含:
背景信息(你在做什么)
当前情况(遇到了什么问题)
独立问题(你想知道什么)
这不是过度设计,而是血的教训。早期版本没有这个要求,结果专家模型经常给出完全无关的建议(因为它不知道你在说什么)。
1.5 并行处理:多 Subagent 同时工作
最让我兴奋的功能是并行 Subagent。
想象这个场景:你需要重构三个模块。传统方式是一个一个来,但我们的TaskTool支持启动多个后台 Subagent:
// AI 可以在一个响应中发起多个任务 yield* launchAgent("重构模块 A", "模块 A 的具体任务...", "general-purpose") yield* launchAgent("重构模块 B", "模块 B 的具体任务...", "general-purpose") yield* launchAgent("重构模块 C", "模块 C 的具体任务...", "general-purpose") // 这三个 agent 会并行运行技术上是怎么实现的?
每个 Subagent 都是独立的 async 操作,我们用一个Map来追踪它们的状态:
// src/utils/session/backgroundTasks.ts (简化) const backgroundTasks = new Map<string, AgentTask>() async function upsertBackgroundAgentTask(task: AgentTask) { backgroundTasks.set(task.agentId, task) // UI 可以轮询这个 Map 来显示进度 }用户可以用TaskOutput工具随时查看结果:
@task-output (block=false) # 查看状态但不等待 @task-output (block=true) # 等待完成这带来的体验提升是巨大的。
以前重构三个模块要串行等,现在可以同时启动,然后去喝杯咖啡,回来一次性看三个结果。
二、智能补全:从"机械匹配"到"读心术"
2.1 用户的真实痛点
我观察了自己和其他开发者使用 AI 助手的习惯,发现一个有趣的现象:
没人喜欢打完整的命令。
输入
dao-qi-harmony-designer?太长,谁记得住?输入
@run-agent-再去翻列表?太慢,打断思路输入
dq希望匹配dao-qi?理所当然的期望
但官方实现的补全是严格前缀匹配:dq只能匹配以dq开头的东西。
这就像你搜索"苹果",结果只给你"苹果电脑",不给你"苹果手机"。
2.2 我们的目标:像读心一样懂用户
我们想要的补全系统是这样的:
缩写支持:
dq→dao-qi连字符感知:
daoqi→dao-qi-harmony-designer数字后缀智能:
py3→python3(不是python35)上下文自动识别:输入
gp5自动知道是@ask-gpt-5(加前缀)Unix 命令库:输入
git自动提示常见命令(commit、push、status...)
这需要融合多种匹配算法。
2.3 算法融合的实践
我们最终融合了 7 种算法:
| 算法 | 用途 | 示例 |
|---|---|---|
| 精确匹配 | 快速路径 | git→git |
| 前缀匹配 | 基础需求 | gi→git |
| 连字符分割 | 处理复合词 | daqi→dao-qi |
| 缩写展开 | 处理简写 | dq→dao-qi |
| 子串匹配 | 模糊查找 | harmony→dao-qi-harmony-designer |
| Levenshtein 距离 | 容错 | gitt→git |
| 数字后缀处理 | 版本号智能 | py3→python3(不是python35) |
关键是怎么组合这些算法。
我们的策略是优先级管道:
// src/utils/fuzzyMatcher.ts (简化示意) function matchCandidate(input: string, candidate: string): Score { // 1. 精确匹配(最高优先级) if (input === candidate) return Score.MAX // 2. 前缀匹配 if (candidate.startsWith(input)) return 0.9 // 3. 连字符分割匹配 const hyphenScore = matchWithHyphenAwareness(input, candidate) if (hyphenScore > 0.7) return hyphenScore // 4. 缩写匹配 const abbrScore = matchAbbreviation(input, candidate) if (abbrScore > 0.6) return abbrScore // 5. 子串 + Levenshtein return matchFuzzy(input, candidate) }每个算法返回一个 0-1 的分数,我们按分数排序,取前 10 个结果。
数字后缀处理是个特殊坑。
早期版本有个 bug:用户输入py3,结果匹配到了python35而不是python3。
问题在于我们的子串匹配算法把3当作了普通字符,没有理解这是"版本号"。
修复方案是添加特殊规则:
function handleNumericSuffix(input: string, candidate: string): Score { // 提取数字后缀 const inputSuffix = extractTrailingNumber(input) const candidateSuffix = extractTrailingNumber(candidate) if (inputSuffix !== null && candidateSuffix !== null) { // 比如输入 "py3",候选是 "python35" // 3 != 35,直接拒绝 if (inputSuffix !== candidateSuffix) { // 但如果是 "python3" 的后缀,那就接受 const candidateWithoutSuffix = candidate.replace(/\d+$/, '') if (candidateWithoutSuffix + inputSuffix === candidate) { return 1.0 // 完美匹配 } return 0 // 不匹配 } } return matchNormal(input, candidate) }2.4 Unix 命令库:从零开始
补全系统的另一个痛点是:系统命令。
用户输入git,希望看到的是git commit、git push这些常用命令,而不是系统 PATH 里所有带git的东西。
但问题是:不同系统的命令不一样。macOS 有brew,Windows 可能没有。
我们的解决方案是:
预置 500+ 常用命令库(按优先级排序)
运行时扫描系统 PATH
取交集:只提示既在库里又在本系统的命令
// src/utils/commonUnixCommands.ts (示意) const COMMON_COMMANDS = [ { name: 'git', priority: 100 }, { name: 'npm', priority: 95 }, { name: 'docker', priority: 90 }, // ... 500+ 条目 ] async function getAvailableCommands(): Promise<string[]> { const systemCommands = await scanSystemPATH() const commonCommands = COMMON_COMMANDS.map(c => c.name) // 取交集 return commonCommands.filter(cmd => systemCommands.includes(cmd)) }效果如何?
用户输入g,优先看到git(优先级 100),而不是gnome-terminal(优先级 5)。
输入doc,如果系统里没装 Docker,就不会提示docker(即使它在库里)。
2.5 智能上下文:去掉@的负担
这是我最得意的小设计。
在官方实现里,你要调用 agent 或 model,必须手动加@前缀:
@run-agent-architect @ask-claude-sonnet-4为什么要用户记住这个语法?
我们做了个简单但强大的改动:自动识别 + 自动补全前缀。
当用户输入arch时:
补全系统检测到这是在匹配
run-agent-architect自动识别这是 agent 类型
补全结果直接显示
@run-agent-architect用户按 Tab 或 Enter,自动补全完整格式
代码大概是:
// src/hooks/useUnifiedCompletion.ts (简化) function detectType(candidate: string): 'agent' | 'model' | 'file' | 'command' | null { if (candidate.startsWith('run-agent-')) return 'agent' if (candidate.startsWith('ask-')) return 'model' // ... 其他类型 } function addPrefixIfNeeded(candidate: string, type: string): string { if (type === 'agent') return `@${candidate}` if (type === 'model') return `@${candidate}` return candidate }用户体验的变化:
以前:输入
@run-agent-再翻列表现在:输入
arc直接看到@run-agent-architect
小事?是的。但一天用下来,这些小事能省下几十次 keystroke。
三、流式架构:为什么 AsyncGenerator 是正确选择
3.1 用户的感知
我在测试早期版本时,发现一个让我抓狂的问题:
当 AI 在执行一个长时间任务时,用户完全不知道发生了什么。
比如 AI 在运行一个大型的 bash 命令,或者读取一个大文件。终端就卡在那里,没有进度条,没有提示,没有任何反馈。
用户会想:是死了吗?还是在工作?
更糟糕的是,如果用户等得不耐烦按 Ctrl+C,任务可能已经执行了一半,但没有任何东西保存下来。
3.2 AsyncGenerator 的优势
我们一开始用的是 Promise:
// 早期版本(Promise) async function executeTool(input: Input): Promise<Result> { // 执行任务 const result = await doWork(input) return result } // UI 层 const result = await executeTool(input) renderResult(result) // 只能在结束时显示结果这有个致命问题:你只能在结束时显示结果。
我们改成 AsyncGenerator:
// 当前版本(AsyncGenerator) async function* executeTool(input: Input): AsyncGenerator<Progress> { yield { type: 'progress', content: '开始处理...' } // 执行任务 const result = await doWork(input) yield { type: 'result', data: result } } // UI 层 for await (const progress of executeTool(input)) { if (progress.type === 'progress') { renderProgress(progress.content) // 实时显示进度 } else if (progress.type === 'result') { renderResult(progress.data) // 显示最终结果 } }效果是立竿见影的:
用户看到实时进度("正在读取文件... 50%")
可以随时按 Ctrl+C 取消(Generator 会自动清理)
增量结果显示(不需要等全部完成)
3.3 进度节流的坑
但 AsyncGenerator 也有个坑:如果你 yield 太频繁,UI 会卡死。
早期我们的 Subagent 工具每执行一个工具就 yield 一次进度。结果有个任务启动了 100 个文件操作,UI 直接被进度更新淹没,终端卡死了。
我们加了节流机制:
// src/tools/agent/TaskTool/TaskTool.tsx:650-798 (示意) const PROGRESS_THROTTLE_MS = 200 // 200ms 节流 let lastProgressEmitAt = 0 const recentActions: string[] = [] // 最多记录 6 个最近动作 for await (const message of queryFn(...)) { // 追踪最近的工具调用 if (message.type === 'assistant') { for (const block of message.message.content) { if (block.type === 'tool_use') { recentActions.push(summarizeToolUse(block.name, block.input)) if (recentActions.length > 6) { recentActions.shift() // 只保留最近 6 个 } } } } // 节流:至少 200ms 才 yield 一次 const now = Date.now() if (now - lastProgressEmitAt >= PROGRESS_THROTTLE_MS) { yield { type: 'progress', content: renderProgress(recentActions) } lastProgressEmitAt = now } }效果:
用户看到的是"每 200ms 更新一次"的平滑进度
进度文本包含最近 6 个动作("Read file A, Edit file B, Grep pattern C...")
不会被大量更新淹没
3.4 取消处理的细节
AsyncGenerator 的另一个优势是取消处理。
用户按 Ctrl+C 时,我们需要:
停止当前的工具调用
清理临时文件
保存已完成的工作
用 Promise 这很难做,因为你不知道任务执行到哪一步了。
但 Generator 可以在每个 yield 点检查取消信号:
async function* executeTool(input: Input, abortSignal: AbortSignal) { yield { type: 'progress', content: '开始...' } // 检查取消 if (abortSignal.aborted) { yield { type: 'cancelled' } return } // 执行第一步 const step1 = await doStep1(input) // 再次检查 if (abortSignal.aborted) { cleanupStep1(step1) // 清理 yield { type: 'cancelled' } return } // 执行第二步 const step2 = await doStep2(step1) yield { type: 'result', data: step2 } }这在处理长时间任务时特别重要。
比如 Subagent 启动了 3 个文件操作,用户在第 2 个时取消了。我们只需要回滚第 1 个操作,第 2 和第 3 个根本没开始。
四、Fork Context:Subagent 的上下文隔离艺术
4.1 问题背景
当主 Agent 启动一个 Subagent 时,有个关键问题:
Subagent 需要多少父对话的上下文?
给太少,Subagent 不知道任务背景;给太多,可能包含不兼容的工具调用(父 Agent 用了某个工具,但 Subagent 工具集里没有)。
4.2 我们的方案:Fork Context
我们设计了一个叫"Fork Context"的机制:
找到 TaskTool 的调用点在父对话消息日志中
只取之前的消息作为上下文
用特殊标记隔离,告诉 Subagent "这些只是背景"
代码示意:
// src/tools/agent/TaskTool/TaskTool.tsx:192-283 function buildForkContextForAgent(options: { enabled: boolean, prompt: string, toolUseId: string, // TaskTool 的调用 ID messageLogName: string, forkNumber: number }) { if (!options.enabled) { return { forkContextMessages: [], promptMessages: [userPrompt] } } // 从日志中找到 TaskTool 的调用点 const mainMessages = readJsonArrayFile(getLogPath(options.messageLogName)) const toolUseIndex = mainMessages.findIndex(msg => msg.message.content.some(block => block.type === 'tool_use' && block.id === options.toolUseId ) ) // 只取之前的消息 const forkContextMessages = mainMessages.slice(0, toolUseIndex) // 添加隔离标记 const forkMarker = createUserMessage(` ### FORKING CONVERSATION CONTEXT ### - The messages above are from main thread - They are provided as context only - Some tools may not be available in sub-agent context ### END FORK CONTEXT ### `) return { forkContextMessages, promptMessages: [forkMarker, userPrompt] } }这解决了几个问题:
Subagent 不会看到父 Agent 的工具调用结果(避免困惑)
Subagent 有足够的上下文理解任务
父对话的工具不兼容不会影响 Subagent
4.3 配置化的灵活性
不是所有 Subagent 都需要 Fork Context。有些简单的任务(比如"计算这个表达式的值")根本不需要上下文。
所以我们在 Agent 配置中加了开关:
// agent 配置示例 { "name": "simple-calculator", "forkContext": false, // 不需要上下文 "tools": ["Calculator"] } { "name": "code-reviewer", "forkContext": true, // 需要上下文(比如之前的讨论) "tools": ["Read", "Grep", "Lsp"] }这让系统既强大又高效。
五、MCP 集成:动态工具的挑战
5.1 MCP 是什么?
MCP (Model Context Protocol) 是 Anthropic 开发的一个协议,允许 AI 助手动态加载外部服务提供的工具。
比如你有个 GitHub MCP 服务器,它可以提供createIssue、listPRs等工具。AI 助手在运行时连接这个服务器,就能使用这些工具。
但这有个技术挑战:TypeScript 的静态类型系统。
5.2 静态 vs 动态
我们的工具系统是基于 TypeScript 接口的:
interface Tool { name: string inputSchema: ZodSchema call(input: z.infer<this['inputSchema']>): AsyncGenerator<...> }这要求每个工具在编译时就有明确的类型。
但 MCP 工具是运行时动态加载的,我们怎么知道它长什么样?
5.3 我们的方案:运行时桥接
我们用 Zod schema 作为"动态类型"的桥梁:
// MCP 服务器返回工具描述 { "name": "github_create_issue", "inputSchema": { "type": "object", "properties": { "title": { "type": "string" }, "body": { "type": "string" } } } } // 我们把它转成 Zod schema const zodSchema = z.object({ title: z.string(), body: z.string() }) // 创建一个包装工具 const mcpTool: Tool = { name: "github_create_issue", inputSchema: zodSchema, async *call(input) { // 调用 MCP 服务器 const result = await mcpClient.callTool(this.name, input) yield { type: 'result', data: result } } }这样,动态工具就能无缝融入静态系统。
5.4 错误处理的坑
MCP 工具可能随时失败(网络断开、服务器崩溃、权限问题)。
我们给每个 MCP 工具包装了一层错误处理:
async function* wrapMcpTool(tool: MCPTool, mcpClient: MCPClient) { try { // 心跳检查 if (!await mcpClient.ping()) { throw new Error('MCP server not responding') } const result = await mcpClient.callTool(tool.name, input) yield { type: 'result', data: result } } catch (error) { // 友好的错误提示 yield { type: 'error', message: `MCP tool '${tool.name}' failed: ${error.message}` } } }用户不会看到原始的 stack trace,而是清晰的错误信息。
六、性能优化的那些细节
6.1 Bun vs Node.js
Kode 优先使用 Bun 运行时,而不是 Node.js。
为什么?
在我们的测试中,Bun 的启动速度比 Node.js 快3-5 倍:
$ time node cli.js --version node cli.js --version 0.45s user 0.12s system 102% cpu 0.558 total $ time bun cli.js --version bun cli.js --version 0.12s user 0.08s system 95% cpu 0.210 total对于一个 CLI 工具,启动速度是用户体验的第一道门槛。没人愿意等半秒钟才看到提示符。
但我们也保留了 Node.js 兼容。
如果用户的系统里没有 Bun,我们会自动降级到 Node.js 20.18.1+:
// cli.js (智能包装器) const hasBun = await commandExists('bun') const runtime = hasBun ? 'bun' : 'node' execSync(`${runtime} src/entrypoints/cli.tsx`, { stdio: 'inherit' })6.2 缓存策略
我们用了多层缓存来提升性能:
| 缓存类型 | 位置 | 生命周期 | 用途 |
|---|---|---|---|
| 模型配置 | 内存 | 进程级 | 避免重复读取配置文件 |
| 工具列表 | 内存 | 会话级 | 工具发现成本高,只做一次 |
| 文件内容 | 内存 | 会话级 | AI 可能多次读取同一文件 |
| MCP 连接 | 内存 | 会话级 | MCP 服务器连接成本高 |
关键是缓存失效策略:
// 文件缓存示例 const fileCache = new Map<string, { content: string, timestamp: number }>() async function readFileWithCache(path: string, freshTime: number = 5000) { const cached = fileCache.get(path) // 如果缓存未过期,直接返回 if (cached && Date.now() - cached.timestamp < freshTime) { return cached.content } // 否则重新读取 const content = await fs.readFile(path, 'utf-8') fileCache.set(path, { content, timestamp: Date.now() }) return content }freshTime 的选择是关键。
太短(比如 100ms),缓存基本没用;太长(比如 1 分钟),用户修改文件后 AI 可能看不到。
我们选择 5 秒,这是基于观察:大部分情况下,AI 在 5 秒内不会多次读取同一文件,但如果用户修改了文件,5 秒后 AI 就能看到新内容。
6.3 增量渲染
对于大文件或长输出,我们不会一次性渲染全部内容,而是增量渲染:
// UI 组件示例 function StreamingOutput({ stream }) { const [output, setOutput] = useState('') useEffect(() => { const reader = stream.getReader() async function read() { while (true) { const { done, value } = await reader.read() if (done) break // 增量更新 setOutput(prev => prev + value) } } read() }, [stream]) return <Text>{output}</Text> }用户看到的体验是:
AI 开始生成答案时,文本就逐步出现
不用等到全部生成完才显示
类似 ChatGPT 的流式效果
七、踩过的那些坑
7.1 Windows 支持的血泪史
最初 Kode 只支持 Unix 系统(Linux/macOS),因为我们大量使用了 Unix 特有的命令和路径。
然后用户开始提 issue:"Windows 上跑不了啊!"
我们踩的第一个坑是路径分隔符。
Unix 用/,Windows 用\。早期代码直接拼接路径:
// 错误示例 const filePath = workspaceDir + '/' + relativePath // Windows 上会出问题修复方案是使用path模块:
import path from 'path' const filePath = path.join(workspaceDir, relativePath) // 跨平台第二个坑是 shell 命令。
Unix 的rm -rf在 Windows 上不存在(Windows 用del或Remove-Item)。
我们写了一个shell 抽象层:
// src/utils/shell.ts (示意) const shellCommands = { removeFile: process.platform === 'win32' ? ['del', '/F'] : ['rm', '-f'], listDirectory: process.platform === 'win32' ? ['dir'] : ['ls', '-la'], // ... } async function removeFile(path: string) { const cmd = shellCommands.removeFile await execa(cmd[0], [...cmd.slice(1), path]) }第三个坑是终端编码。
Windows CMD 默认用 GBK 编码,而我们用 UTF-8。这会导致中文路径乱码。
解决方案是在启动时设置编码:
// Windows 特殊处理 if (process.platform === 'win32') { process.env.POWERLINE_COMMAND = 'echo' process.env.LANG = 'en_US.UTF-8' }现在 Kode 在 Windows 上可以原生运行了。
7.2 query.ts 文件过大的教训
我们的query.ts文件(核心 AI 编排逻辑)长到了37KB。
这是个严重的代码坏味道。文件太大意味着:
难以理解(一次只能看一部分)
难以测试(依赖太多)
难以维护(改动可能影响其他功能)
我们计划在下一版本拆分它:
query.ts (当前 37KB) ├── query-core.ts # 核心循环 ├── query-tools.ts # 工具调用逻辑 ├── query-context.ts # 上下文管理 ├── query-permissions.ts # 权限检查 └── query-streaming.ts # 流式处理教训:当一个文件超过 500 行时,就该考虑拆分了。
7.3 Zod Schema 的性能坑
我们大量使用 Zod 来验证工具输入。但有个问题:Zod 的验证成本不低。
早期版本,每次工具调用都要重新验证 schema:
// 低效版本 async function callTool(tool: Tool, input: unknown) { // 每次都解析 schema const schema = tool.inputSchema const validated = schema.parse(input) // ... }优化方案是缓存解析后的 schema:
// 高效版本 const schemaCache = new WeakMap<Tool, z.ZodType>() async function callTool(tool: Tool, input: unknown) { let schema = schemaCache.get(tool) if (!schema) { schema = tool.inputSchema schemaCache.set(tool, schema) } const validated = schema.parse(input) // ... }这减少了约 30% 的工具调用开销。
八、未来的坑和机会
8.1 待解决的挑战
挑战 1:上下文压缩
随着对话增长,上下文会越来越大。即使我们有自动裁剪,但裁剪策略很简单("删掉最旧的消息")。
未来我们想实现更智能的压缩:
识别关键信息(用户的需求、重要的决策)
压缩冗余内容(重复的文件读取、无关的尝试)
保留结构化的摘要
挑战 2:工具组合
目前工具是独立调用的。但有些任务需要组合多个工具:
1. Grep 找到所有 TODO 2. 对每个 TODO,Read 相关文件 3. 用 AskExpertModel 分析每个 TODO 的优先级 4. 用 TodoWrite 更新优先级我们想让 AI 能自动组合这些步骤,而不是用户一步步指导。
挑战 3:本地模型支持
目前 Kode 只支持 API 模型。但很多用户想要本地模型(隐私、成本、离线使用)。
本地模型的挑战是:
模型文件很大(几 GB)
推理速度慢(没有 GPU)
量化后的质量损失
我们正在调研 Ollama 和 LM Studio 的集成。
8.2 令人兴奋的方向
方向 1:多模态支持
AI 模型开始支持图像输入。我们想让 Kode 能:
读取截图(比如错误信息截图)
分析图表(比如性能监控图)
生成图片(比如架构图)
方向 2:语音交互
在终端里用语音输入指令,听起来很科幻,但在某些场景下很实用:
手不在键盘上
快速记录想法
多任务操作
方向 3:协作功能
如果多个开发者同时用 Kode 在同一个项目上工作,他们可以:
共享 Agent 会话("你看看我让 AI 生成的这个架构")
协作编辑("我改了这个文件,你让 AI review 一下")
知识共享("这个问题的解决方案要保存到项目知识库")
更多AIGC文章
RAG技术全解:从原理到实战的简明指南
更多VibeCoding文章