公司动态

给编码代理加一双“眼睛”:deepseek harness 识屏插件实战

📅 2026/8/31 21:26:18
给编码代理加一双“眼睛”:deepseek harness 识屏插件实战
如果你的编码代理一直跑在终端里屏幕对你来说就只是摆设。上周我改一个前端暗色主题的细节deepseek harness 在终端里跑得很顺代码改完、测试通过但它没法告诉我按钮在暗色模式下到底好不好看。于是我做了一件有点“野”的事给这套 harness 写了一个识屏插件让它能在处理任务前先截一张当前屏幕把看到的内容变成上下文。这件事做完以后我发现问题不在于“AI 能不能看图”而在于一套原本只处理文本的工作流需要怎样接入视觉信息。识屏插件的价值不是给 agent 多一只眼睛而是把现实屏幕上的信息重新拉回文本和工具调用的循环里。表面看是一个小工具背后涉及模型能力、上下文拼装、本地代理、错误排查和工程化边界。这篇记录一下一个像 catch 一样的插件是怎么被一步步磨出来的。1. 为什么缺“识屏”这个能力会让本地编码代理变得不完整1.1 agent 只能看到文本屏幕是另一个世界先说一个很容易被忽略的事实deepseek harness 这类编码代理本质上生活在一个纯文本世界里。它能看到什么仓库文件、终端输出、测试报告、日志、你输入的指令。它看不到什么浏览器渲染出来的真实界面、弹出来的报错窗口、设计稿的排版、IDE 的高亮颜色。对 agent 来说这些都不存在。这不是模型能力的问题而是输入通道的问题。OpenAI 那套 API 格式、Anthropic 的 messages 结构、DeepSeek 的 OpenAI 兼容接口核心都是文本 token 的交换。你可以给模型传图片前提是模型本身支持视觉输入并且你的调用方真正把图片放进了正确的字段。大部分编码代理默认不会自动截屏也不会把屏幕截图塞进请求里。所以当 agent 说“改完了”而你需要确认页面效果时它只能告诉你它改了哪些代码无法告诉你页面看起来怎么样。你问它“你看一下屏幕”它只能回你一个礼貌的抱歉。这个缺口看似很小但一旦你经常做前端、爬虫、自动化验收、界面 bug 修复就会变成每天都要踩的坑agent 的工作链路里缺的不只是眼睛而是“屏幕上下文”这个入口。1.2 deepseek harness 的核心工作流是一套本地执行链路要理解识屏插件为什么可行先要理解 deepseek harness 到底是什么。它不是一个魔术盒也不是 DeepSeek 官方发布的一个大型桌面应用。更准确地说它是一套社区里常见的编码代理方案以开源 CLI 代理为骨架把模型 Provider 配置成 DeepSeek通过本地代理或者配置切换让终端里的 agent 用 DeepSeek 模型驱动完成读仓库、改代码、跑测试、提交改动这类工程任务。很多人喜欢把它理解成“Codex 接入 DeepSeek”的一种玩法。Codex 本身是 OpenAI 的编码代理形态但作为开源组件它的模型 Provider 是可以替换的。deepseek harness 就是这套替换实践的产物用 DeepSeek 的 API 作为大脑用本地 CLI 作为手和脚。这个方案的价值有几个层面成本DeepSeek 比常见海外模型便宜太多适合长时间挂着让 agent 反复改代码。本地可控它的配置、日志、请求链路都在本地你能看到每一轮请求到底发了什么。模型可换harness 这个英文词本身就有“套具、控制装置”的意思在 agent 语境里它定义的是执行循环感知、决策、行动、观察。但正是“本地可控”这个优点让插件化成为可能。如果它是一个黑盒 SaaS你很难在请求发出前插入一个截图步骤。而 deepseek harness 这类方案通常允许你在调用链路的某个环节塞自己的逻辑。这也就解释了为什么识屏插件值得做因为你控制的不是一个远程服务而是一条本地执行链路。1.3 识屏解决的不是“截图”而是上下文断裂好现在把问题说透。假设你正在做一个前端页面还原。你给 deepseek harness 发指令按这份设计稿把登录页的间距调一下。设计稿是一张图。agent 如果只处理文本它就没有“看到”这张图如果你把图片路径给它它可能会尝试用文件读取工具读图片但大多数纯文本模型会把图片当成二进制数据读不出语义。再比如你在跑一个桌面应用程序弹了一个错误窗口这个窗口的内容不在终端输出里不在日志文件里只在屏幕上。agent 看到测试失败了却看不到错误弹窗。你手动把文字敲给它效率就下来了。这些场景的共同点是什么工作流中间断了一截。人类是通过屏幕感知世界的而 agent 是通过文本感知世界的。识屏插件做的事情就是把屏幕上的内容重新转换成 agent 能消费的输入无论是文字还是图片。所以识屏插件真正的价值不是“给 AI 加一双眼睛”而是把被断开的上下文重新接回 agent 的感知循环里。明白了这一点你才能理解后面每一步实现选择——为什么不能直接截个大图丢给它为什么要在 OCR 和视觉模型之间做取舍为什么要在 harness 的扩展点里注册工具而不是魔改源码都是为了让这条上下文链路稳定、可控、可维护。2. 先从一次失败尝试说起识屏插件不是简单截个图2.1 第一版实现截图、编码、塞进消息刚开始我很天真。我的想法非常简单注册一个get_screen_info工具让 agent 在需要的时候调用它然后截屏、保存、读成 base64、塞进 user 消息的image_url字段。这不就完了吗第一版代码长这样import { execFileSync } from node:child_process; import { readFileSync, writeFileSync } from node:fs; import { tmpdir } from node:os; import path from node:path; function captureScreen() { const file path.join(tmpdir(), harness-screen-${Date.now()}.png); if (process.platform darwin) { execFileSync(screencapture, [-x, file]); } else if (process.platform win32) { // Windows 走 PowerShell 截屏 } else { // Linux 尝试 gnome-screenshot 或 grim } return file; } function imageToBase64(file) { return readFileSync(file).toString(base64); }然后我把它接进工具调用截图 → base64 → 塞进消息 → 调模型。看起来逻辑完整。如果用的模型本身支持视觉输入这一步通常就能跑通。但问题恰恰出在“通常”这两个字上。2.2 撞上的问题thinking 模式必须回传 reasoning_content第一次完整运行直接 400。错误长这样cc switch local proxy failed while handling codex endpoint /responses. provider: deepseek; upstream_status: http 400; cause: the reasoning_content in the thinking mode must be passed back to the api.看到这个报错的瞬间我的第一反应是“代理挂了”但仔细看cause那一段问题其实出在多轮消息结构上。DeepSeek 的推理模型在工作时会返回一个reasoning_content字段里面是模型自己的思考过程。如果你用 thinking 模式下一轮请求时这个reasoning_content必须被原样带回给 API否则 API 会视为非法请求。而我在识屏插件里做的事情本质上是在“上一次模型回复”之后插入了新的 user 消息里面带着截图。问题就出在我在拼装新消息时没有把上一轮的reasoning_content正确保留或者在某个版本里插件把旧的 assistant 回复重新组装但漏掉了这个字段。这种错误最讨厌的地方在于它不是识屏逻辑本身的错误而是上下文拼装环节踩到了模型的服务端约束。后来我反复测试发现这类“thinking mode 必须回传 reasoning_content”的要求在推理类模型里不算少见。只要你的 harness 允许自定义上下文修改就很容易踩到这个坑。2.3 排查链路先用最小请求确认再扩大改造那次失败以后我做了一个很机械但很有用的排查。顺序是去掉识屏插件恢复 harness 的默认调用。确认默认链路没问题。单独用脚本调 DeepSeek API把上一轮的reasoning_content原样带回确认 API 可以通过。用调试面板看实际请求体对比“默认请求”和“接了识屏插件之后的请求”找出消息结构到底差在哪。一点一点加回插件的逻辑直到定位到是某个字段被覆盖。如果你也遇到类似的 400按这个顺序走通常能很快定位。不要一开始就在插件里改来改去那样很容易把问题绕晕。还有一点很重要不要直接去改 harness 核心代码。一旦你改了源码下次更新工具就会冲突而且你很难判断是官方逻辑的问题还是自己改动的问题。插件的价值是“可插拔”不是“改得越深越好”。我知道很多人会觉得“看见屏幕”这件事很惊艳但真正让它可落地的恰恰是这些不惊艳的排查细节。3. 一个可落地的识屏插件结构抓屏、压缩、OCR/编码、拼装上下文3.1 抓屏跨平台的几种通用方式识别屏幕的第一步是拿到屏幕图像。这里没有统一标准不同操作系统有不同命令。macOS 最简单screencapture -x /tmp/harness-screen.pngWindows 上一般用 PowerShell示例结构大概是Add-Type -AssemblyName System.Windows.Forms,System.Drawing $b [System.Windows.Forms.Screen]::PrimaryScreen.Bounds $bmp New-Object System.Drawing.Bitmap $b.Width, $b.Height $g [System.Drawing.Graphics]::FromImage($bmp) $g.CopyFromScreen($b.Location, [System.Drawing.Point]::Empty, $b.Size) $bmp.Save($env:TEMP\harness-screen.png)Linux 桌面环境比较碎常见的是gnome-screenshot或者grim视你用的桌面环境而定。在 Node 里封装一层很容易做成平台无关的调用import { execFileSync } from node:child_process; import path from node:path; import os from node:os; export function captureScreen() { const file path.join(os.tmpdir(), harness-screen-${Date.now()}.png); if (process.platform darwin) { execFileSync(screencapture, [-x, file]); } else if (process.platform win32) { execFileSync(powershell, [-File, path.join(__dirname, capture.ps1), file]); } else { execFileSync(gnome-screenshot, [-f, file]); } return file; }如果只需要截某个窗口可以进一步限定窗口 ID 或坐标区域。这个我没有在插件里做得太复杂但设计上应该留出参数。注意截屏属于敏感操作。插件默认只截全屏可能是最省事的但最负责任的做法是让用户配置“截图区域”而不是什么都抓。3.2 图像预处理为什么不能直接塞原图第一版我直接拿 4K 截图转 base64结果一测就发现问题图片太大请求体动辄几 MBAPI 延迟高token 消耗也莫名其妙地上去了。所以后来我加了一个预处理步骤缩放最长边压到 1024 或 768。这个尺寸对大多数视觉识别的需求都够了还能显著减少 token。换编码如果不是必须保留透明背景优先用 JPEG质量 80。PNG 的 base64 体积通常比 JPEG 大很多。裁剪如果只需要某块区域提前裁掉无关区域信息更干净。一个简单的处理思路from PIL import Image def preprocess_screen(image_path, max_side1024, quality85): img Image.open(image_path) img.thumbnail((max_side, max_side)) if img.mode in (RGBA, P): img img.convert(RGB) output_path image_path.replace(.png, .jpg) img.save(output_path, JPEG, qualityquality) return output_path预处理的核心不是“压缩”而是让模型看到它真正需要的信息同时不把无关像素浪费在 token 里。3.3 两条喂图路线多模态直读 vs OCR 转文本做完预处理之后就要决定把什么内容喂给模型。这里有两套路线选择取决于你的模型是否支持视觉输入。路线输入形式优点局限适合场景多模态直读image_urlbase64模型能看到布局、颜色、文字、图片细节要求模型支持视觉输入token/延迟较高前端还原、视觉验收、设计图比对OCR 转文本纯文本块成本低、兼容普通文本模型、上下文稳定丢失布局和颜色识别可能有误差报错弹窗、终端输出、文档信息提取我建议的做法是优先走 OCR 转文本因为 deepseek 主流文本模型便宜、快、稳定。OCR 后的文字能直接塞进系统提示词或 user 消息不依赖视觉能力。但如果你确实需要让 agent“看懂”页面布局比如让它判断按钮是否居中、卡片间距是否一致那 OCR 不够必须走视觉模型直读。一个折中方案是把两种模式都做成插件选项route: ocr默认抓屏 → OCR → 返回文本。route: vision抓屏 → 压缩编码 → 返回 image_url。route: both两个都返回。这样你在不同任务里可以自由切换而不用改插件代码。3.4 上下文拼装示例无论走哪条路最终都要把结果拼进模型请求。如果模型支持多模态输入标准结构是{ role: user, content: [ { type: text, text: 这是当前屏幕截图请根据看到的内容继续处理。 }, { type: image_url, image_url: { url: data:image/jpeg;base64,BASE64 } } ] }如果走 OCR 文本就非常简单{ role: user, content: 当前屏幕 OCR 内容如下\nTEXT }注意一个问题不要每次调用都把这轮截图永久留在上下文里。识屏结果应该是“当前这一轮”的临时上下文用完就清理。否则几百轮对话下来图片的 token 累积会让上下文爆炸成本也会失控。4. 在 deepseek harness 里把它接进去自定义工具而不是魔改 CLI4.1 先搞清 harness 暴露的扩展点hooks 还是 toolsdeepseek harness 这类工具通常不会提供一个“插件市场”让你一键安装。它的扩展点一般有两种hooks在特定时机执行脚本比如每次请求前、响应后。tools向 agent 注册一个可被调用的外部工具函数。这两种方式各有适用场景。hooks 更像“监听器”适合做日志、拦截、注入环境信息tools 更像“手”适合做实际动作。我最后选择的是 tools。原因很简单我不希望每次请求都强制截屏而是希望模型在需要时主动去调用截屏工具。在系统提示词里加一句规则当用户提到屏幕、界面、报错弹窗、页面效果、视觉验证时先调用 get_screen_info 工具再回答问题。这样模型自己会决定什么时候看屏幕而不是每轮都盲截一张图浪费 token 和时间。4.2 一个通用接入方式作为外部工具注册具体到实现我在自己的插件里注册了一个外部工具。大致结构是这样const screenTool { name: get_screen_info, description: Capture the current screen and return OCR text or image. Use when user asks about screen, UI, popup, page preview., parameters: { type: object, properties: { route: { type: string, enum: [ocr, vision, both], default: ocr } }, required: [] }, async run({ route }) { const imagePath captureScreen(); const processedPath preprocess(imagePath); if (route vision || route both) { const base64 imageToBase64(processedPath); return { image_base64: base64, ...(route both ? { ocr_text: ocr(processedPath) } : {}) }; } return { ocr_text: ocr(processedPath) }; } };具体注册位置在不同版本里不一样有的在 plugin 目录有的在配置文件里声明 tools。落地前请先看对应版本的 README 或--help不要照抄网上的旧代码。这里要特别说明这段代码不是某款工具的官方 API而是一个比较通用的工具注册结构。它的意义在于帮你理解“识屏插件在 harness 里的定位”而不是告诉你某个具体版本应该怎么写。4.3 加一个本地调试面板用 dsh web 看每一轮请求写完插件之后我开始频繁使用一个本地调试面板。在我用的方案里入口是pnpm dsh web。它会把每一轮请求的输入、输出、耗时、报错都显示出来。这个面板对我的帮助特别大。因为当你把屏幕信息插入到请求里时你非常需要确认一件事截图内容到底有没有被正确送到模型那边。如果模型返回 400你可以在面板里看请求体。对比“加了插件”和“没加插件”的请求很快就能发现是哪个字段被覆盖了是reasoning_content丢了还是messages顺序乱了。如果你用的 harness 没有类似面板至少要在插件里保留请求日志。logs/ screen-tool.log request-body.log errors.log不要小看日志。识屏插件一旦跑起来失败模式比普通工具复杂得多——截图可能失败OCR 可能返回空API 可能 400请求体可能因为上下文过长被截断。没有日志你只能瞎猜。5. 识屏插件真正适合的场景和不适合的场景5.1 已经验证有效的几个场景第一个场景是前端代码修改后的自检。以前 agent 改完样式我只能心累地看。现在它改完之后可以自己调用识屏工具截一张浏览器里的实际渲染效果然后判断“间距是否合理”“是否居中了”“是否被遮挡”。如果用了视觉模型它甚至能直接指出哪里不对。第二个场景是读取系统级报错弹窗。很多桌面端工具的报错并不进入终端而是弹在屏幕上层。普通测试输出看不到日志里也可能没有。识屏插件通过 OCR 把弹窗文字提取出来agent 就能准确理解错误内容。第三个场景是用设计图指导编码。如果你截一张设计稿给 agent并配上视觉模型它可以根据屏幕上的设计稿来调页面。这个场景对多模态支持的要求比较高但对前端开发的吸引力也最大。5.2 不建议做的场景识屏插件不是万能的。以下场景我明确不建议使用场景为什么不建议实时连续桌面监控截图频率一高token 和延迟直接爆炸且安全风险不可控高权限交互界面自动化一旦 agent 看到敏感内容可能误触发危险操作纯文本任务强行识屏只会增加延迟和错误率没有收益银行、密码、内部系统页面截图内容可能包含敏感数据不建议发给任何外部模型服务屏幕内容转发到不可信服务如果插件把截图上传到你无法控制的 API 或存储风险很高这里做一个原则性表格比参数更重要判断问题通过标准这个信息能通过文本拿到吗能则不要截屏任务是一轮一轮执行的吗是识屏合适需要连续流不合适截图内容允许发到当前模型服务端吗不允许则只 OCR 本地处理或不做模型支持视觉输入或 OCR 够用都不支持识屏无意义5.3 一个简单的判断框架要不要做识屏先回答三个问题我后来总结出三个问题每次想给 harness 加识屏能力时先自问一遍这个信息能不能先从文本拿到如果可以优先文本。截图是最后手段。任务是不是快照式的识屏适合“看一眼当前状态然后做判断”的场景不适合持续追踪动态画面。你承担得起截图带来的上下文和隐私成本吗如果截图内容敏感或模型不支持视觉就要换方案。这三个问题能过滤掉至少一半“想给 harness 加识屏”的冲动。它不是每时每刻都需要但当你需要的时候它是唯一能接上上下文的方式。6. 如果你也想写自己的 harness 插件建议按这个顺序来6.1 先用默认配置跑通最小任务不要一上来就写识屏插件。先确认你的 harness 环境本身没问题模型配置正确API 能连通README里的最小示例能跑通。比如让 agent 读一个文件改一个函数跑一次测试。这个步骤看起来多余但它能隔离问题。如果你在最开始就引入插件遇到一个 400你很难判断是插件的问题还是环境的问题。只有默认链路稳定了你才有资格谈扩展。6.2 用一条样例确认输入通道不要直接把插件完整写完。先把“截图”这一步单独跑一遍把“OCR”这一步单独跑一遍把“调用 API”这一步单独跑一遍。我的流程是先手动截一张图确认文件存在。再手动跑一次 OCR确认输出文本正确。用脚本构造一个最小请求把 OCR 文本发给模型确认返回正常。最后才把这三步串进 harness 的工具调用链。每个环节单独验证能让你在后续排查时立刻知道是哪一环坏了。6.3 识别扩展机制而不是抄一堆社区片段deepseek harness 这类工具更新很快不同版本的 hooks 和 tools 接口可能完全不同。如果你在网上看到一个旧插件片段不要直接复制进去。先做三件事打开本地安装后的源码或类型定义找到工具注册入口。看示例配置里有没有声明自定义 tools 的地方。用最小改动跑通一个“hello world”工具比如返回当前时间然后再替换成识屏逻辑。很多人的插件问题不是逻辑写错了而是注册方式不对。这一点比识屏本身更重要。6.4 日志、错误重试、上下文管理是长期使用门槛工具能跑起来和能长期使用完全是两回事。识屏插件一旦放进日常开发流就会遇到各种脏场景截图命令在某些环境被权限拦截。OCR 在低分辨率下返回空字符串。API 因为上下文中包含图片而产生更高的 token 成本。上一轮截图内容没有清理导致后续请求越来越大。所以我建议至少在插件里做到# 每次抓屏后保留原始文件路径 # 每次 OCR 后写入 result 到日志 # API 400 时记录 request body 的前 200 个字符 # 上下文清理策略识屏结果只在当前轮有效当一个工具同时具备“干净的输入、可观测的执行过程、明确的失败反馈、可清理的副作用”时它才算从实验 hack 变成了真正的工程工具。6.5 从一次性插件走向可复用工具最后一步是参数化。不要把你的截图区域写死不要把你的输出模式写死。用配置控制screen.capture.area全屏还是指定区域。screen.capture.routeocr / vision / both。screen.capture.maxSide最长边。screen.capture.qualityJPEG 质量。screen.context.expire识屏结果保留几轮。把这些参数从代码里拿出来放进配置文件你才能把它复用到其他项目上。这才是插件真正“可复用”的样子。7. 收尾工具的价值不只是“看见屏幕”做这个识屏插件最大的收获并不是“我的 agent 能看见屏幕了”。真正让我想明白的是另一件事agent 工具的进化方向不是越来越像人而是越来越能补上工作流里断掉的环节。一个能改代码的代理很好但它再强也看不到你屏幕上的报错弹窗一个能调 API 的代理很好但它读不到设计稿的排版偏差。识屏插件解决的不是“视觉能力”而是“输入通道”。这件事反映出来的趋势更值得关注deepseek harness 这类本地编码代理正在把 agent 从“一个固定的黑盒助手”变成“一条你可以自己改造的工作流基础设施”。今天你可以给它加识屏明天你可以给它加语音输入、加定时任务、加浏览器控制、加自定义工具。它的边界不是官方功能列表而是你对工作流的理解。我现在的建议很具体如果你也一直在用编码代理先不要急着写复杂插件。找一个小到不能再小的痛点比如“每次跑完前端都想让 agent 看一眼页面”从一条截图命令开始把链路跑通再说。识屏这条路最难的不是截图不是 OCR也不是拼装上下文而是你终于意识到工作流断了的地方才是工具最该生长的地方。