不换IDEA也能用上AI智能体:DeepSeek Harness插件实战指南
现在 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 17IDEA 2024 之后官方要求 17部分新版本需要 21Gradle 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 idcom.example.harness-assistant/id nameHarness Assistant/name vendorexample/vendor dependscom.intellij.modules.platform/depends extensions defaultExtensionNscom.intellij /extensions actions /actions /idea-plugincom.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 idcom.example.HelloAction classcom.example.HelloAction textHello Harness descriptionTest action add-to-group group-idEditorPopupMenu anchorfirst/ /action /actions右键编辑器就能看到它。这一步跑通之后说明开发环境、编译流程、插件加载机制都是好的接下来写真正的功能才不会在一个坏地基上折腾。3. 智能体接入的第一个关键环节把 IDEA 的代码上下文喂给 Harness3.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 做灰色预览而不是直接写入文档。原因是直接写文档会污染用户的输入历史CtrlZ 退不干净用 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 每敲一个字就卡死一次。原因是这种回调默认跑在 EDTEvent 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-chat300~800ms快够用聊天问答/代码解释deepseek-chat300~800ms日常主力复杂重构/多步修改deepseek-reasoner3~5s质量高延迟可接受单元测试生成deepseek-chat1~2s配合工具调用使用这套分工的好处是日常操作不被慢速推理拖累遇到真正复杂的问题时又可以切到更强的推理模型。模型切换只需要改 Harness 侧配置插件完全不用动这就是中间层带来的灵活性。6.3 后续打算报错自动修复、MCP 工具、团队共享这个插件离我理想中的形态还有一段距离。下一步我准备做三件事第一把 IDEA 的编译错误自动收集并塞给 Harness让 Agent 自己定位问题、给出修复补丁第二在 Harness 里接入更多本地工具尤其是跑测试和查 git diff让 Agent 能从“会读代码”进化到“会验证自己的改动”第三把插件打成签名 jar 包放到团队内部仓库里让同事直接安装就能连上公司内部的 Harness 服务。这个项目做下来我最大的体会是AI 编程插件拼的其实不是模型本身而是上下文工程和交互设计。模型再强如果它不知道你光标停在哪个文件、哪段代码有问题回答就只能是“正确的废话”。把 IDE 的信号喂给智能体再把智能体的能力落回编辑器这条路还有很大的空间可以走。我现在已经习惯了在 IDEA 里选中报错直接丢给 Harness 处理这个周末项目算是真正进了我的日常开发流程。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →