🌱 手把手做一只「会改代码」的 VS Code AI 小助手

11 阅读8分钟

引言

不是带你读某一个 repo,而是把实现思路拆开讲清楚
读完你也能自己搭一个,顺便养一只听话的 Agent 🐣

聊天机器人和编程助手,差在哪呀?

很多人做 AI 助手的第一步是:接个大模型 API,把回复显示出来。恭喜,你已经拥有一个可爱的聊天窗啦 ✨

但编程助手还想再多一点点:它不只会说话,还要会动手——读文件、改代码、创建新文件。

这就引出了 Agent:

普通 ChatAgent
输出纯文字文字 + 工具调用
能力只能「说」还能「做」
场景问答、解释重构、批量改文件

今天这篇,就用「做一个能改代码的 VS Code 小助手」当主线,把里面值得学的模式都拎出来~

一、先画一张地图:三层小蛋糕 🍰

做 VS Code AI 助手,几乎都会长成这样:

🍰 顶层:Webview(React 聊天界面,好看但没文件系统权限)

↕ postMessage(唯一小桥)

🍰 中层:Extension Host(能调 VS Code API,真正读写文件)

↕ HTTP

🍰 底层:LLM API(大脑,负责想)

为什么要拆开?

  • Webview 住在沙箱里,安全但手短
  • Extension Host 权限完整,能打开编辑器、读写磁盘
  • 所以常见分工是:UI 负责好看,Extension 负责干活

这不是某个项目的奇技淫巧,而是 VS Code 生态里很经典的玩法哦~

二、通信协议:先把「快递单」设计好 📦

两端不能互相 import,只能靠 postMessage。新手最容易写成字符串满天飞,后期改起来会哭。

可复用小技巧:抽一层 shared 协议

  1. 消息类型常量,确保全局唯一
enum WebviewToExtensionKey {
    SendMessage = 'send-message',
    PickFiles = 'pick-files',
    PauseStreaming = 'pause-streaming',
}
  1. 消息体类型定义
type SendMessage = {
    type: 'send-message';
    text: string;
    attachments?: FileAttachment[];
};

ExtensionWebview 共用同一份类型,改协议时 TypeScript 会帮你抓漏——超级贴心 💕

消息大致分两类:

  • Webview → Extension:用户操作(发消息、选文件、暂停)
  • Extension → Webview:状态推送(新消息、流式 chunk、历史)

经验小贴士: 协议越早定越好。等功能堆成小山再补,很容易同一种事出现三种 message type 😵‍💫

三、Agent 循环:让模型学会「想 → 做 → 再想」🔄

Agent 的核心,其实是一个很温柔的小循环:

用户消息进来
↓
问 LLM:「要不要用工具?」
├─ 要 → 执行工具 → 结果塞回对话 → 再问一次
└─ 不要 → 输出最终文字 → 收工!✨

伪代码长这样:

for (let step = 0; step < maxSteps; step++) {
    const { content, toolCalls } = await completeWithTools({ messages, tools });
    // 模型决定调用工具
    if (toolCalls?.length) {
        // 通知webview工具调用
        messages.push({ role: 'assistant', content, tool_calls: toolCalls });
        // 遍历调用工具
        for (const call of toolCalls) {
            // 工具调用结果
            const result = await executor.execute(call.name, args);
            // 传递给webview
            messages.push({ role: 'tool', tool_call_id: call.id, content: result});
        }
        continue; // 带着结果继续问
    }
    if (content) return content; // 任务完成啦
}

几个值得学的小心机:

  1. 一定要有maxSteps
    模型有时候会陷入「读完再读再读」的无限摸鱼,上限是生产级 Agent 的基本礼貌 🐟

  2. 工具结果必须回传
    role: tool 让模型基于真实结果继续决策,而不是瞎编「已经改好啦」

  3. System Prompt 要和工具一起设计
    比如:改已有文件优先 edit_file;用户附件路径必须原样使用。Prompt 和工具是一对小情侣,单改一边容易吵架 💑

maxSteps

maxSteps 就是 Agent 这一轮最多能「问 LLM → 调工具」循环几次

可以把它理解成:防止模型无限摸鱼的刹车 🛑

1 轮:问 LLM → 调工具
第 2 轮:带着结果再问 → 再调工具
...
第 N 轮:还没给出最终文字 → 停下来报错

在项目里:

  • 配置项:chatSample.agent.maxSteps
  • 默认值:8
  • 范围:1 ~ 20
  • 读取位置:getMaxAgentSteps() → 传给 runAgentLoop

为什么需要它?

模型有时会陷入「读文件 → 再读 → 再读」或反复改同一处,如果没有上限,Agent 可能一直转下去,又慢又费 token。

所以:

  • maxSteps = 8:最多允许 8 次工具决策轮次
  • 某轮没有 tool_calls、直接返回 content:任务正常结束,不会硬跑满 8 次
  • 跑满了还没最终回复:抛出类似 Agent exceeded maximum steps 的错误

maxSteps 不是「必须执行几次」,而是「最多允许循环几次」。

completeWithTools

Agent 用来「问一次 LLM」的封装函数

可以把它理解成:

带着「对话历史 + 可用工具清单」去问模型,
模型要么回一段文字,要么说「请帮我调用这些工具」。

它做了什么?

  1. 读取 API Key / model / baseUrl
  2. 调用 OpenAI 兼容接口 chat.completions.create
  3. 把工具定义 tools 一起传给模型(Function Calling)
  4. 返回两个东西:
{
  content: string | null,      // 模型的文字回复
  toolCalls: ... | null        // 模型想调用的工具列表
}

和普通聊天有什么区别?

普通聊天 streamChatcompleteWithTools
是否带工具是(tools
返回内容纯文字(可流式)content + tool_calls
流式(等整次回复)
用途打字机式对话Agent 决策一轮

tool_choice: "auto" 的意思是:模型自己决定「直接回答」还是「先调工具」。

在 Agent 循环里扮演什么角色?

AgentLoop
  └─ completeWithTools()   ← 问 LLM:下一步干嘛?
       ├─ 有 toolCalls → 执行工具,再问下一轮
       └─ 只有 content → 任务完成,返回最终文字

所以它不是「执行工具」的,而是 让模型做决策 的那一层;真正读写文件的是后面的 ToolExecutor

四、工具设计:读、写、改,三种小手 ✋

文件类 Agent 通常至少需要三个工具:

工具人设适用场景
read_file悄悄看书的内向同学先看看再改
write_file新建派小能手创建或整文件覆盖
edit_file精细手术刀按片段精确替换

edit_file 的「精确替换」特别值得学:比整文件 rewrite 更稳,也更省 token。

实现上就是找 old_string,校验唯一性,再替换:

if (occurrences === 0) throw new Error('找不到这段文字');
if (!replaceAll && occurrences > 1) throw new Error('不唯一,请加更多上下文');

**安全层千万别省! **

Extension 里做路径校验:相对路径禁止 ..扩展名白名单限制在工作区根目录内

记住口诀:模型不可信,执行层要可信 🛡️

五、流式 UI:Agent 的输出不只是「打字机」⌨️

纯 Chat 的流式很简单

token 来了 → 推到 UI → 逐字显示

Agent 更热闹一点:除了最终文字,还有工具状态、步骤计划,这些是结构化的,不是纯 Markdown

有个超级实用的模式叫:

在文本通道上承载结构化流(NDJSON)

Extension 推给 Webview 的 chunk 可能长这样:

{"type":"steps","id":"s1","steps":[{"title":"编辑 index.ts","status":"pending"}]}

{"type":"tool","id":"t1","name":"edit_file","status":"running"}

{"type":"tool","id":"t1","name":"edit_file","status":"done","result":"..."}

Webview 端逐行 JSON.parse

  • idtool → 合并更新(running → done
  • steps → 渲染步骤条
  • 解析不了的行 → 当 Markdown 文本

**为什么不直接传对象? **

因为 Agent 输出是渐进式的:

工具开始 ➡️ 工具结束 ➡️ 步骤变化 ➡️ 都是分多次推

把增量 append 到一个 text 字段,历史存储和流式协议就能统一成「一个字符串」——实现更轻松,扩展也更友好 🌸

六、步骤条:让 Agent 不再黑盒 📋

用户看 Agent 改代码,最怕两件事:

  1. 不知道它在干嘛
  2. 不知道还要等多久

步骤条就是来拯救信任感的~

两种生成方式可以一起用:

**方式 A:模型主动规划 **

调工具前先输出一行:

{"type":"steps","steps":[{"title":"编辑 index.ts"},{"title":"创建 config.js"}]}

**方式 B:从工具调用反推 **

模型没规划也没关系,根据 write_file / edit_file 自动生成标题。

状态流转:

pending → running → done / error 

read_file 可以当「幕后工作者」——参与决策,但不占步骤位,避免步骤条被「读取 xxx」刷屏 🙈

这是 Agent UX 里很值得抄的一点:把 planningexecution 可视化,用户会安心很多。

七、文件附件:给 AI 塞一张小纸条 📎

用户把文件附到消息上,常见两种做法:

做法优点缺点
只传路径,让 Agent 自己读省 token多一轮工具调用
发送时注入文件内容模型立刻有全文大文件费 token

第二种可以拼成这样的上下文块:

<file_context>

<file path="src/index.ts">

// 完整内容

</file>

</file_context>

再在 System Prompt 里写清楚:用附件路径,别在别的地方重复创建。

这是典型的 Prompt + 工具 + UI 三角配合:

  • UI 提供附加能力
  • Extension 注入上下文
  • Prompt 约束模型行为

缺一角,体验就容易歪掉~

八、暂停/恢复:双端缓冲的小温柔 ⏸️

流式暂停看起来简单,实现却容易踩坑:

  • Extension 还在推,Webview 已暂停 → 可能丢 chunk
  • Webview 缓存了,Extension 以为结束了 → UI 卡住

稳妥做法:两端都备一个小缓冲

暂停时:

Extension → 写入 _chunkBuffer
Webview → 写入 chunkCache

恢复时:

两边一起 flush,再处理 StreamEnd

这是分布式系统里「背压」的迷你版,做 Chat 产品迟早会遇到,趁早学会不吃亏 💡

九、构建:两个产物,一条流水线 🔧

VS Code 扩展 + React Webview,通常是双构建:

webpack → dist/extension.jsExtension Host)

vite → dist/webview/ (聊天 UI

加载 Webview 时要用 webview.asWebviewUri() 转换资源路径,并注入 CSP
本地开发记得先 build Webview,不然侧边栏会空白——这是新人高频踩坑点,抱抱~ 🥹

十、你可以直接带走的小 Checklist ✅

想自己做一只?按这个顺序推进比较稳:

  1. Webview 聊天 UI + postMessage 双向通信
  2. LLM 纯文本对话(先不做 Agent
  3. StreamStart / Chunk / End,做流式展示
  4. 定义 1~2 个工具,实现 Agent 循环
  5. 加路径校验和扩展名白名单
  6. NDJSON 推送工具状态,Webview 解析渲染
  7. (可选)步骤条、文件附件、暂停恢复
  8. (可选)真 token 流式,替换最终回复的一次性输出

一步一步来,小助手会越养越乖~

希望你带走的不该只是「某个文件夹叫什么」,而是这些可以换到 JetBrains 插件、Electron 桌面应用、甚至 Web IDE 的通用思路。

欢迎继续折腾,养出你自己那只好看又靠谱的 AI 小助手 🐣✨