现在 AI 编程工具卷到什么程度,估计不用我多说。Qoder、Trae 这类产品把“在编辑器里直接和 Agent 对话改代码”做成了标配,团队里已经有不少同事直接切到 AI IDE 上。但问题是,我日常主力开发还是在 IntelliJ IDEA 上,项目里攒了几年的快捷键肌肉记忆、自定义代码模板、团队共享的代码风格,不是说换就能换的。于是我把思路换了一个方向:不换 IDE,让 IDEA 自己去接一个智能体。
这里就要说到 DeepSeek Harness。你可以把它理解成模型之上的智能体编排层:模型调用、上下文窗口管理、工具注册、会话状态都归它管。单独的桌面端和命令行模式我都跑过,聊聊天、跑跑 Agent 流程都没问题。但它最大的短板是:它读不到我 IDE 里正在打开的文件、光标位置、选中区域,更没法把生成的补全内容直接落回代码编辑器。所以我花了两个周末,把 Harness 通过服务模式暴露成 HTTP 接口,再写了一个 IntelliJ IDEA 插件,做了类 Qoder 的三件事:行内补全、侧边聊天、选区操作。整体跑通之后,体验已经很接近那些全家桶 AI IDE 了。
如果你也在纠结“要不要为了 AI 功能换 IDE”,或者想在团队内部把智能体接到现有开发工具链里,又或者单纯想学 IntelliJ 插件开发,这篇应该能帮你省不少时间。我不会只讲思路,会把能直接复用的步骤和踩过的坑都写下来。
1. 为什么放着现成的 AI IDE 不用,偏要自己搓一个插件
1.1 Qoder 这类工具带火的需求是什么
我观察到一个现象:Qoder 这类产品真正打动人的,不是“多了一个聊天窗口”,而是它让模型第一次有了 IDE 的上下文感知。你选中一个方法,它能就地解释逻辑;你光标停在一个报错行,它能顺着上下文给出修复方案;它甚至能自己读文件、改文件、执行命令。这种形态下,模型不再是“你问一句、它答一句”的问答机器人,而是真正在帮你干活。
但随之而来的问题是:迁移 IDE 的成本被严重低估了。团队内部可能有统一的代码风格插件、公司私有的静态检查规则、内部框架的代码模板,这些在 IntelliJ IDEA 里沉淀了很久,换到一个新的 AI IDE 上不一定能全部带过去。很多时候,为了一个 AI 功能让全组人改开发环境,阻力远比想象中大。所以“在自己的 IDE 里接一个智能体”这条路,反而是现阶段成本最低、收益最直接的做法。
1.2 为什么中间要套一层 DeepSeek Harness,而不是直接调 API
有人可能会说:我直接用 DeepSeek 的 API,再写个 IDEA 插件,不就行了?我一开始也这么想,后来发现这里面有一个被很多人忽略的问题:你要做的不只是“调用模型”,而是“让 Agent 用代码工作”。
如果你只调 API,那么会话上下文、工具调用协议、多轮对话的状态管理、模型返回的 function call 解析,全部要自己在插件里实现。这还没算上未来想切换模型、想接 MCP 工具、想给 Agent 增加本地能力这些扩展需求。越往后做越会发现,你实际上是在重复造一个智能体框架。
DeepSeek Harness 的价值就在这里,它把这层已经做完了。模型无关的接入、上下文窗口管理、工具注册、会话编排,都是框架层面的事。我的插件只需要做三件事:采集 IDE 的信号,把信号转成 Harness 需要的输入,再把输出渲染回编辑器。这样责任边界很清晰,插件本身不会越来越臃肿。
1.3 这个插件具体要解决哪三个问题
我把整个项目拆成了三个问题,也对应着三条实现主线:
- IDE 侧:监听编辑器事件,知道用户当前在编辑什么文件、光标在哪、选中了什么、最近改了哪里。
- 网关侧:把 Harness 起成 HTTP 服务,插件侧不依赖模型 SDK,只和这个服务通信。
- 交互侧:做三类入口——编辑器内的行内补全、侧边聊天面板、右键选区操作菜单。
这三个问题分别对应了代码结构里的三个模块,后面我会逐个展开。这个拆分方式也让我在开发的时候有个很清晰的节奏:先跑通网关,再写 IDE 采集,最后做交互界面。
2. 开工前的准备工作:从 JDK 到第一个能弹窗的插件工程
2.1 环境版本怎么选最稳
写 IntelliJ IDEA 插件,本质上是基于 IntelliJ Platform 做扩展开发,所以版本匹配很关键。我自己用的组合是这样:
- IntelliJ IDEA Community Edition 2024.2 作为编译和调试目标
- JDK 17(IDEA 2024 之后官方要求 17+,部分新版本需要 21)
- Gradle 8.x 配合 org.jetbrains.intellij 插件
新建项目的时候,直接在 IDEA 里选择 IDE Plugin 模板就行,这会帮你把 Gradle 配置和 plugin.xml 的骨架都生成好。有一个容易被忽略的地方是:要建的是插件工程,而不是普通 Java 工程,两者在 Gradle 配置上差别很大。
2.2 build.gradle.kts 里那几个参数的含义
我第一次创建插件工程时,对intellij配置块的几个参数也是一头雾水。实际上它控制的是:以哪个版本的 IDE 作为依赖来编译你的插件。我用的配置大概是这个样子:
plugins { id("java") id("org.jetbrains.intellij") version "2.1.0" } group = "com.example" version = "1.0.0" repositories { mavenCentral() } dependencies { // 尽量少引第三方库,后面会讲原因 } intellij { type.set("IC") // IC 表示 Community Edition version.set("2024.2.3") // 你要依赖的 IDE 版本 } patchPluginXml { sinceBuild.set("242") untilBuild.set("") }这里的type为什么用 IC 而不是 IU?因为 IC 是社区版,免费而且对插件开发者友好。如果你的插件在 IC 上能跑,那在大部分 Ultimate 版本上通常也能跑。sinceBuild我填的是 242,对应 2024.2 的主版本号,这个值如果填太高,低版本 IDE 就装不上你的插件,填太低又可能用到新 API 导致低版本启动报错。
2.3 plugin.xml 里的依赖声明是灵魂
插件工程里有一个META-INF/plugin.xml文件,它是插件的身份证。新人最容易踩的坑就是漏掉 depends 声明,或者搞不清楚该声明什么。我的 plugin.xml 开头是这样的:
<idea-plugin> <id>com.example.harness-assistant</id> <name>Harness Assistant</name> <vendor>example</vendor> <depends>com.intellij.modules.platform</depends> <extensions defaultExtensionNs="com.intellij"> </extensions> <actions> </actions> </idea-plugin>com.intellij.modules.platform是最基础的模块,相当于插件运行的地基。如果你还要操作 Java 语言相关的 PSI 结构、识别 Java 文件,那要加上com.intellij.modules.java。刚开始我只加了 platform,发现读 Java 文件类型时行为很怪,查了半天才发现是依赖缺失。
2.4 第一个能弹窗的 Action
在写复杂功能之前,我建议先注册一个最简单的 Action,验证整个链路是通的。创建一个类继承 AnAction,在 actionPerformed 里弹个消息框:
public class HelloAction extends AnAction { @Override public void actionPerformed(@NotNull AnActionEvent e) { Project project = e.getProject(); Messages.showInfoMessage(project, "Harness Assistant is running", "Hello"); } }然后在 plugin.xml 的<actions>里注册:
<actions> <action id="com.example.HelloAction" class="com.example.HelloAction" text="Hello Harness" description="Test action"> <add-to-group group-id="EditorPopupMenu" anchor="first"/> </action> </actions>右键编辑器就能看到它。这一步跑通之后,说明开发环境、编译流程、插件加载机制都是好的,接下来写真正的功能才不会在一个坏地基上折腾。
3. 智能体接入的第一个关键环节:把 IDEA 的代码上下文喂给 Harness
3.1 先解决“智能体不知道你在看什么”的问题
很多套壳插件做得像智障,根源在于它只知道用户敲了什么字,不知道用户在干什么。我要做的第一步,就是让插件主动采集 IDE 里的信号:
- 当前打开文件的绝对路径和语言类型
- 光标所在行号、当前选中区间的起始和结束偏移量
- 选中的文本内容(如果有)
- 当前文件的代码片段,按光标位置截取前后窗口
- 项目名、模块名、构建工具类型
采集这些信息用的是 IDEA 的 Editor 和 Document 机制。核心思路是:拿到 Editor 对象,从 CaretModel 拿光标位置,从 Document 拿文本内容。用一个上下文对象把这些信息串起来,序列化成 JSON,作为请求 Harness 的输入。
这里有一个很重要的工程习惯:不要在任何 UI 事件回调里直接做耗时操作,采集上下文也是同理。正确的做法是:在事件回调里只做“采样”,把需要的数据复制出来,然后用异步任务去构建上下文、请求模型。否则 IDE 会卡到你怀疑人生,这个坑后面我会单独展开。
3.2 上下文打包策略:不要无脑把整个文件塞给模型
一个文件几千行很常见,但模型的上下文窗口是有限的,而且塞得越满,模型对关键信息的注意力越分散。我采用的策略是按优先级分配 token 预算,用一张表来管理:
| 信息类型 | 采样方式 | token 预算 |
|---|---|---|
| 选中的文本 | 原样携带 | 最多 2048 |
| 当前文件内容 | 光标前后各 100 行,带行号 | 最多 4096 |
| 项目信息 | 模块名、项目名、语言、构建工具 | 128 |
| 同目录相关文件 | 只带文件名列表,不展开内容 | 256 |
| 编译错误 | 只带当前文件相关的 Error 条目 | 1024 |
如果超了预算,就优先截断当前文件内容,而不是砍掉选中文本。因为这个方案的核心假设是:用户选中的东西,就是他当下最关心的东西,永远不能丢。
关于 token 估算,我后来发现一个经验算法就够了:中文大约 1.5 到 2 个字一个 token,英文大约 3.5 到 4 个字符一个 token。我会在预估结果上再留 20% 的缓冲,宁可少传一点,也不要触发模型端的 context length exceeded。
3.3 Harness 服务模式:把插件变成纯粹的 HTTP 客户端
DeepSeek Harness 的部署形式,我这里不展开细说,安装文档里一般都会说明怎么把服务跑起来。我在开发时是把 Harness 的 HTTP 服务跑在本地 127.0.0.1 的某个端口上,然后用插件和它通信。
为什么选择这种方式,而不是把 Harness 的 SDK 直接打进插件 jar 里?原因有两个:一是插件 jar 的体积会膨胀,而且 SDK 依赖很容易和 IDE 自带的类库冲突;二是服务化之后,模型切换、工具配置、上下文策略都只需要改 Harness 那边,插件一行代码都不用动。
通信协议我选的是 OpenAI 兼容的 Chat Completions 接口。好处很明显:不需要引入任何私有的 SDK,只要构造 JSON 请求就能和 Harness 对话,而且 DeepSeek API 本身就兼容这套协议,调试的时候可以直接拿 curl 验证。
HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .build(); String payload = """ { "model": "deepseek-chat", "messages": [%s], "stream": true, "tools": [%s] } """.formatted(messagesJson, toolsJson); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(baseUrl + "/v1/chat/completions")) .header("Content-Type", "application/json") .header("Authorization", "Bearer " + apiKey) .POST(HttpRequest.BodyPublishers.ofString(payload)) .build();这里有三个要点:stream一定要设成 true,否则首字延迟会很难看;tools是给 Harness 用的工具声明,后面会说怎么用它让 Agent 有能力读文件;baseUrl和apiKey必须做成可配置项,不能写死在代码里。
3.4 配置项必须可改,否则插件没法交付
刚开始我把 baseUrl 写死成http://127.0.0.1:8080,自己开发没问题,但一旦要把插件分享给同事,问题就来了:每个人的机器上 Harness 服务端口可能不一样,API Key 也要区分。所以一定要用 IDEA 的 PersistentStateComponent 机制,把 baseUrl、apiKey、model 三个配置项持久化下来,并在设置界面提供编辑入口。
这一步做起来其实不算复杂,就是定义一个状态类,标注 @State 注解,然后在插件激活时读取。做完之后,插件才不是一个只能在自己电脑上跑的 Demo。
4. 类 Qoder 的三个核心功能落地:补全、聊天、选区操作
4.1 行内补全:从监听输入到自动应用
行内补全是最有“智能感”的功能,也是实现细节最多的一个。我的实现思路是:
监听 Document 的变更事件,用户停止输入大概 1 秒后,触发一次补全请求。请求的上下文是光标前 200 个字符加上光标后 100 个字符,让模型补全中间这段代码的延续。拿到结果之后,不是直接插入编辑器,而是先在内存里计算生成内容和现有代码的差异,确认没有重叠冲突,再应用进去。
这里我强烈建议用 Inlay Text 做灰色预览,而不是直接写入文档。原因是直接写文档会污染用户的输入历史,Ctrl+Z 退不干净;用 Inlay 预览的话,用户按 Tab 才真正写入,按 Esc 直接取消,交互上和 Cursor 这类产品保持一致。为了做这个,我踩了一个比较久的坑:默认的 Inlay 会一直在,用户继续输入时不会自动消失,需要监听输入事件手动清理。
还有一个很容易被忽视的问题是:不要在用户正在快速输入的时候频繁触发补全。我用了一个简单的防抖,用 ScheduledExecutorService 延迟 1 秒执行,每次有新输入就取消前一个任务重新计时。这样既不会卡手,也不会浪费请求。
4.2 侧边聊天面板:Tool Window 与流式输出的处理
聊天面板是另一个核心入口。IDEA 插件里做侧边栏的标准方案是 Tool Window,注册方式是在 plugin.xml 里声明 toolWindow 扩展,然后实现 ToolWindowFactory 接口。
UI 层面我没有引入特别重的组件,就用 JEditorPane 配合 HTML 来渲染聊天内容。JEditorPane 对 ``` 代码块的支持需要自己做一点处理:先按代码块把文本拆开,代码块部分用等宽字体和灰底背景,普通文本部分用普通样式。这个方案虽然简陋,但稳定,不会因为引入 Markdown 渲染引擎而把插件体积撑大。
流式输出是聊天面板体验的关键。Harness 返回的内容是 SSE 流,也就是一段一段的增量文本。如果等整个回答都生成完再显示,用户会盯着空白界面等很久。我的做法是:每收到一个增量 chunk,就把它追加到当前回答的缓冲区中,然后通过 ApplicationManager.getApplication().invokeLater 把增量同步到 UI 线程。注意,千万不要在每个 chunk 都重新 setText 整个文档,那样长回答会越渲染越卡。正确做法是只 append 新增部分,并维护一个 StringBuilder 作为后端数据源。
4.3 右键选区操作:让功能入口离用户更近
聊天和补全覆盖了大多数场景,但还有一个高频操作是“选中代码之后做点什么”。我在编辑器右键菜单里注册了几个 Action,分别对应不同 prompt:
- 解释选中代码:输出这段代码在做什么、为什么这么写
- 生成单元测试:根据选中方法的输入输出生成测试用例
- 重构建议:分析选中代码的坏味道,给出改进方案
实现这些 Action 的逻辑是共通的:读取选中文本,加上当前文件上下文,调用 Harness,把结果展示到聊天面板里。区别只在于 system prompt 不同。我在定义这些 prompt 的时候会比较讲究措辞,比如生成单测时会要求“先列举需要 mock 的依赖,再写测试代码”,这样 Harness 的输出更有结构,而不是一上来就堆代码。
4.4 工具调用闭环:让 Agent 有手有脚
做完上面三个功能,这个插件其实还只是一个“高级聊天助手”,离类 Qoder 的 Agent 体验还差一步:让模型能自己去读文件、查代码、看项目结构。这一步靠的是 Harness 的工具调用能力。
我的开放思路是:在插件这边实现一批只读工具,模型返回 tool_call 的时候,插件在本地沙箱执行这些工具函数,然后把结果作为新的消息喂回模型。第一批工具我只做了三个:
- list_files:列出指定目录下有哪些文件
- read_file:读取指定文件的指定行区间
- search_symbol:在项目里搜索类名或方法名
安全方面我有一条铁律:所有写操作,包括写文件、删除文件、执行终端命令,都必须弹出确认窗口让用户点同意。这个确认机制不是对用户的不信任,而是防止模型在长对话中产生不可预期行为。毕竟工具调用的本质是代码执行,权限边界一开始就要收紧,后续再逐步放开。
5. 调试插件时踩过的真实坑:ClassLoader、UI 线程和流式响应
5.1 插件启动就 NoSuchMethodError,问题出在依赖冲突
我在第一个版本里引入了 OkHttp 做 HTTP 客户端,结果插件一启动就报 NoSuchMethodError,一度以为是自己代码写错了。排查到最后才发现:IntelliJ IDEA 自己内置了 OkHttp 的旧版本,我打包进插件的 OkHttp 新版本和它撞车了。
这个问题的本质是类加载器冲突。IDEA 插件默认是独立的 classloader,但 IDE 平台自身的类对插件是可见的,如果你的第三方库和平台冲突了,就会出现各种奇怪异常。我的解决办法是:删除 OkHttp,改用 JDK 内置的 java.net.http.HttpClient。这个内置类没有任何外部依赖,永远不会有版本冲突。从那以后我定了一个规矩:IntelliJ 插件能少引第三方库就少引。
5.2 在监听回调里做网络请求,IDE 直接冻死
这是我踩过最狠的坑。一开始我在 DocumentListener 的回调里直接发 HTTP 请求,结果 IDEA 每敲一个字就卡死一次。原因是:这种回调默认跑在 EDT(Event Dispatch Thread),也就是 UI 线程上。在 UI 线程上做网络请求,等于把整个 IDE 的界面线程阻塞在那里等网络返回。
这个问题的正确解法很简单:事件回调里只采集数据和状态,真正的网络请求提交到异步线程池,收到响应后再用 invokeLater 切回 UI 线程更新界面。这不是什么高深技巧,但在实际开发中极其容易被忽略,原因是你本地测试时网络延迟低,卡顿可能在几十毫秒内就过去了,等用户现场体验出问题才意识到。
5.3 流式输出丢字和光标乱跳
聊天面板做好之后,测试时发现一个诡异的问题:长回答偶尔会丢字,而且滚动条会随机跳回顶部。我一开始以为是 Stream 读取的问题,后来发现是多个线程并发写 JTextPane 导致的状态混乱。
解决思路有两步:第一,所有对文本组件的更新操作都走同一个 UI 线程调度入口;第二,不要每次收到小 chunk 就全量刷新 UI,而是要维护一个回答缓冲区,在 UI 线程里只 append 这次新增的文本。滚动条固定在底部也需要处理,要判断用户是否正在往上翻历史记录,如果用户在翻旧内容,就不要强制拉到底部。
5.4 上下文超限:问大文件就报错
用 Harness 问一个 2000 行的大文件时,经常出现 context length exceeded。排查下来发现,还是上下文打包策略不够精细。前期我只做了“按行数截断”,但不同文件的行长差异很大,有的文件 100 行只有几百 token,有的文件 20 行就有几千 token。
后来我补了一个 token 估算层,在发送前先估算整个上下文的 token 数,如果超过模型窗口的 80%,就按优先级逐级裁剪:先砍项目信息,再砍同目录文件列表,接着压缩当前文件窗口到前后各 50 行,如果还不够,就把文件内容截断并加一行“源文件较长,此段为光标附近片段”的提示词。经过这一轮调整,超限报错基本绝迹。
5.5 插件装不上:since-build 和 until-build 的坑
分享给同事的时候,有人反馈插件在 IDEA 里提示“不兼容”。查下来是这个插件的 since-build 填得太高,而同事的 IDEA 版本相对旧。反过来,如果你的 until-build 填了具体的版本号,到了新版 IDE 上又装不上。我最后直接把 until-build 留空,since-build 填自己开发环境对应的版本号,这样处理最简单。这个字段在 plugin.xml 里看着不起眼,但它直接关系到一个插件能不能装到目标用户的环境里。
6. 实测效果、模型选择和后续打算
6.1 日常使用下来的真实体感
插件跑通之后,我自己高强度用了两周,说实话初期版本挺一般的,补全经常给出“听起来合理但完全是错的”代码。但后面我把上下文打包策略调好、把 tool 工具接入完整之后,质量上了一个台阶。
现在的实际体验是:行内补全生成样板代码、getter/setter、简单 CRUD 非常顺,基本是即写即补;聊天问答解释业务代码特别好用,尤其是一坨没人维护的老项目;选区操作里“生成单元测试”是使用频率最高的,虽然生成的用例偶尔要修一下断言,但至少省了搭测试骨架的时间。
延迟方面,Harness 服务跑在本机、使用 deepseek-chat 模型时,补全首字大概 300 到 800 毫秒,聊天流式很顺滑;如果用 deepseek-reasoner 做复杂重构,首字会到 3 到 5 秒,但推理质量明显更高,尤其是在“多步修改”这类任务上。
6.2 模型分工:不能一个模型打天下
做这个项目给我最直接的感受是:不同任务对模型的要求差异非常大,最好在 Harness 侧配置不同的模型路由。我自己用的是这么一套分工:
| 使用场景 | 推荐模型 | 首字延迟 | 说明 |
|---|---|---|---|
| 行内补全/样板代码 | deepseek-chat | 300~800ms | 快,够用 |
| 聊天问答/代码解释 | deepseek-chat | 300~800ms | 日常主力 |
| 复杂重构/多步修改 | deepseek-reasoner | 3~5s | 质量高,延迟可接受 |
| 单元测试生成 | deepseek-chat | 1~2s | 配合工具调用使用 |
这套分工的好处是:日常操作不被慢速推理拖累,遇到真正复杂的问题时又可以切到更强的推理模型。模型切换只需要改 Harness 侧配置,插件完全不用动,这就是中间层带来的灵活性。
6.3 后续打算:报错自动修复、MCP 工具、团队共享
这个插件离我理想中的形态还有一段距离。下一步我准备做三件事:第一,把 IDEA 的编译错误自动收集并塞给 Harness,让 Agent 自己定位问题、给出修复补丁;第二,在 Harness 里接入更多本地工具,尤其是跑测试和查 git diff,让 Agent 能从“会读代码”进化到“会验证自己的改动”;第三,把插件打成签名 jar 包,放到团队内部仓库里,让同事直接安装就能连上公司内部的 Harness 服务。
这个项目做下来,我最大的体会是:AI 编程插件拼的其实不是模型本身,而是上下文工程和交互设计。模型再强,如果它不知道你光标停在哪个文件、哪段代码有问题,回答就只能是“正确的废话”。把 IDE 的信号喂给智能体,再把智能体的能力落回编辑器,这条路还有很大的空间可以走。我现在已经习惯了在 IDEA 里选中报错直接丢给 Harness 处理,这个周末项目算是真正进了我的日常开发流程。