返回

文本 Agent 遇到图片时缺什么#

一些以文本和代码为主的 Agent 不能直接读取图片。用户给出报错截图、UI 设计稿或图表时,Agent 如果只拿到文件路径,就无法继续判断图片里的事实。

常见做法是让视觉模型先生成一段长描述,再交给主模型。但主模型通常只需要和当前任务有关的少量证据,例如错误文本、坐标、按钮状态或图表数值。

为什么整张图先转成长描述成本高#

常见的解决方案是把图片交给视觉模型,生成一段详细描述,再塞回主模型:

图片 → 视觉模型 → 长描述 → 主模型
plaintext

但这种方法会带来几个直接问题:

问题影响数据
Token 消耗高每次调用 2000-5000 tokens成本高
无关描述多视觉模型输出大量不需要的内容上下文污染
上下文污染主模型上下文被长描述占满性能下降
越权推理视觉模型替主模型做决策责任不清

举个实际例子#

当用户上传一张 400x300 的错误截图时:

传统方案:

视觉模型输出: "这张图片显示了一个终端窗口,其中包含一个 Python
错误信息。错误类型是 ModuleNotFoundError,具体信息是无法找到
名为 'requests' 的模块。错误发生在文件 /Users/user/project/app.py
的第 42 行..."
→ 约 3000 tokens
plaintext

Free Vision Skill:

VEP/1|src=zhipu/glm-4.6v-flash|m=error|a="ModuleNotFoundError: No module named 'requests'"|t="app.py:42"|c=0.97
→ 约 130 tokens
plaintext

这个示例从约 3000 tokens 降到约 130 tokens,约减少 95.7%。它只代表这张截图和这组提示词。


Free Vision Skill 的做法#

Free Vision Skill 把视觉模型的输出压缩成面向当前任务的证据包,再交给文本 Agent 继续推理。

职责边界#

工作流程#

图片 + 问题
  ↓
免费视觉 API(只提取当前任务需要的事实)
  ↓
压缩为 VEP(Visual Evidence Packet)
  ↓
DeepSeek / Codex / Claude Code / OpenCode 继续推理
plaintext

当前实现#

能力当前实现
视觉证据压缩VEP/1 把回答、OCR 文本、对象、问题和关键值压成短记录
Provider fallback按区域与优先级选择 Provider,失败时切换备用项
本地缓存SHA-256 key,TTL 与 LRU 淘汰
凭据存储支持 macOS Keychain、Linux Secret Service 和 Windows Credential Manager
并发控制RequestPool 限制同时请求数并做退避重试
13Provider 数量
24 h缓存 TTL
3最大并发
本文记录的项目配置

核心技术:VEP/1 协议#

VEP = Visual Evidence Packet(视觉证据包)

视觉模型不返回完整分析,只返回事实:

VEP 格式#

VEP/1|src=zhipu/glm-4.6v-flash|m=error|
a="Cannot find module ethers"|
t="src/app.ts:42"|
e=[dependency error]|
c=0.97
plaintext

字段说明#

字段含义示例
VEP/1协议版本VEP/1
srcProvider 和模型zhipu/glm-4.6v-flash
m任务模式error / ocr / ui / chart
a直接答案"Cannot find module"
tOCR 文本"src/app.ts:42"
o关键对象[button, input, modal]
e可见错误[overlapping, clipped]
v关键值["$99", "2024-12-31"]
c置信度0.97
cache缓存状态cache=hit

Token 对比#

场景VEP 大小主模型接收传统方案节省
错误提取~150 chars~50 tokens2000+ tokens97% ⬇️
UI 审计~400 chars~80 tokens3000+ tokens97% ⬇️
OCR 表格~500 chars~120 tokens4000+ tokens97% ⬇️
图表分析~300 chars~70 tokens2500+ tokens97% ⬇️

项目样例中的主模型输入 token

传统方案使用表格中 2000+、3000+、4000+、2500+ 的下限绘制,因此柱长是保守下限,不是精确测量。

错误 VEP50 tokens
本文项目样例。VEP 数值和传统方案下限来自上表。

架构设计#

系统架构#

flowchart TB
    C[CLI commands] --> P[Provider registry]
    P --> K[Cache lookup]
    K --> Q[Request pool and rate limit]
    Q --> V[Vision API]
    V --> E[VEP encoder]
    E --> O[Compact evidence packet]
mermaid

缓存和并发控制位于 Provider 调用前后。VEP 编码只处理已经返回的视觉结果,不负责替主模型做后续决策。

核心模块#

Provider System (src/providers.ts)#

// 13 个 Provider 配置
export function resolveProviderOrder(requested: string, region: Region): ProviderConfig[] {
	if (requested !== 'auto') return [getProvider(requested)];

	// 自动降级:优先同区域,然后 fallback
	const preferred = registry.providers
		.filter((p) => p.region === region)
		.sort((a, b) => a.priority - b.priority);

	const fallback = registry.providers
		.filter((p) => p.region !== region)
		.sort((a, b) => a.priority - b.priority);

	return [...preferred, ...fallback];
}
typescript

智能缓存 (src/cache.ts)#

并发控制 (src/pool.ts)#

class RequestPool<T> {
	// 指数退避重试
	private async execute(item): Promise<void> {
		for (let attempt = 0; attempt <= maxRetries; attempt++) {
			try {
				const value = await this.withTimeout(fn(), timeoutMs);
				return resolve({ success: true, value });
			} catch (error) {
				// 指数退避: 1000ms → 2000ms → 4000ms
				const delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
				await sleep(delay);
			}
		}
	}
}
typescript

VEP 生成 (src/vep.ts)#

export function toVep(result: VisionResult, maxChars: number): string {
	const parts = [
		'VEP/1',
		`src=${result.provider}/${result.model}`,
		`m=${result.mode}`,
		result.answer ? `a="${result.answer}"` : '',
		result.text ? `t="${result.text}"` : '',
		result.issues?.length ? `e=[${result.issues.join(',')}]` : '',
		`c=${result.confidence?.toFixed(2)}`
	]
		.filter(Boolean)
		.join('|');

	return compact.slice(0, maxChars);
}
typescript

本地性能记录#

健康检查优化#

下面的 65 秒和约 2 秒来自项目中的本地样例。它说明串行探测可以改成有限并发,不代表不同网络或 Provider 条件下都能得到 32.5 倍提升。

优化前(v0.3 及以前):

// 串行检查,每个 5s timeout
for (const provider of providers) {
	await checkProviderHealth(provider); // 5s each
	await sleep(1000); // 批次延迟
}
// 13 个 provider × 5s = 65s
typescript

优化后(v0.4):

// 并发批次检查
const batchSize = 3;
for (let i = 0; i < providers.length; i += batchSize) {
	const batch = providers.slice(i, i + batchSize);
	await Promise.all(batch.map((p) => checkProviderHealth(p)));
	await sleep(500); // 批次延迟
}
// 13 个 provider: [3 + 3 + 3 + 1] = ~2s

// 本地样例: 65s -> ~2s
typescript

缓存性能#

缓存命中率取决于图片、问题和 Provider 是否重复。下面的 7/8 只是一组很小的项目测试,不把它写成稳定的 90% 命中率承诺。

TTL + LRU 策略:

TTL: 24 小时
最大条目: 1000
LRU 淘汰: 最少访问优先
plaintext

实测数据:

Hit Rate:     87.5% (7/8)
Misses:       1
Evictions:    0
Size:         8 entries
plaintext

并发控制#

RequestPool 配置:

{
  maxConcurrency: 3,    // 最大并发
  timeoutMs: 30000,     // 超时时间
  maxRetries: 2,        // 最大重试
  baseDelayMs: 1000,    // 基础延迟
  maxDelayMs: 10000     // 最大延迟
}
typescript

指数退避策略:

尝试 1: 失败 → 延迟 1000ms
尝试 2: 失败 → 延迟 2000ms
尝试 3: 失败 → 放弃 (total: 3000ms)
plaintext

实际使用场景#

1. 错误截图分析#

free-vision see --image ./error.png \
  --question "只提取错误信息和行号"
bash

VEP 输出:

VEP/1|src=zhipu/glm-4.6v-flash|m=error|
a="Cannot find module 'lodash'"|
t="webpack.config.js:15"|
e=[module resolution error]|
c=0.98
plaintext

主模型继续推理: “错误是找不到 lodash 模块,在 webpack.config.js:15。 解决方案:运行 npm install lodash”


2. UI 审查#

free-vision see --image ./ui-screenshot.png \
  --question "列出所有被裁切、重叠或禁用的 UI 元素"
bash

VEP 输出:

VEP/1|src=zhipu/glm-4.6v-flash|m=ui|
o=[{name:"Submit",issue:"disabled"},{name:"Avatar",issue:"clipped"}]|
c=0.95
plaintext

3. OCR 表格提取#

free-vision see --image ./table.png \
  --question "提取所有文本和表格结构"
bash

VEP 输出:

VEP/1|src=zhipu/glm-4.6v-flash|m=ocr|
a="Q3 销售报表"|
t=["产品","销售额","增长率"],["A",12000,"15%"],["B",8500,"8%"]|
c=0.92
plaintext

4. 图表数据提取#

free-vision see --image ./chart.png \
  --question "只返回图表标题、趋势和三个关键值"
bash

VEP 输出:

VEP/1|src=zhipu/glm-4.6v-flash|m=chart|
a="月度营收增长"|
v=[45200,58300,72100]|
c=0.96
plaintext

5. 自动裁剪优化#

free-vision see --image ./screenshot.png --auto-crop \
  --question "只提取错误信息"
bash

裁剪结果:

✂️  Cropped: 400x300 → 369x58
   Reduction: 82%
   Saved to: ./screenshot.cropped.png
plaintext

效果: 图片大小减少 82%,进一步降低 token 消耗。


支持的 AI Agent#

Free Vision Skill 不绑定某个主模型,适合所有文本-only 的 Agent:

Claude Code#

# Claude Code Hook 自动检测图片
npx skills add lora-sys/free-vision-skill
bash

Codex#

# 一键安装脚本
curl -fsSL https://raw.githubusercontent.com/lora-sys/free-vision-skill/main/installers/codex-install.sh | bash
bash

OpenCode#

{
	"agents": {
		"coder": {
			"skills": ["free-vision"],
			"vision": {
				"provider": "auto",
				"region": "cn",
				"auto-detect-images": true
			}
		}
	}
}
json

DeepSeek / 其他#

# 通用调用
free-vision see --image ./screenshot.png --question "你的问题"
bash

开发历程与未来规划#

v0.1.0 — MVP(已完成)#

  • Provider registry(13 个 Provider)
  • VEP/1 协议
  • Auto-fallback 降级
  • SHA-256 本地缓存
  • .env 和 Keychain

v0.2 — 集成增强版(已完成)#

  • Claude Code Hook 智能识别
  • Provider 健康检查
  • Codex 一键安装脚本
  • OpenCode Agent 集成

v0.3 — 高级功能(已完成)#

  • Windows Credential Manager
  • VEP Schema Validator
  • Image auto-crop(—auto-crop)

v0.4 — 性能优化(已完成)#

  • Cache TTL + LRU eviction
  • Request pool with configurable concurrency
  • Rate limiter(token bucket)
  • Exponential backoff retry
  • Parallel failover for providers
  • Performance tests(30/30 passing)

v1.0 — 生产就绪(规划中)#

  • 全面的错误处理和恢复
  • 完整的测试覆盖(>80%)
  • 性能监控和日志
  • 企业级安全审计
  • 完整的 API 文档
  • 插件系统

适用边界#

Free Vision Skill 适合主模型本身不直接读取图片,但任务又需要从截图、UI、表格或图表中取得少量可验证信息的场景。它的主要作用是缩短送回主模型的视觉上下文,并把 Provider、缓存和失败切换收在一个工具里。

文中的 token、缓存和时延数字都来自项目样例或本地测试。它们可以说明实现取舍,但不能作为跨模型、跨网络条件的固定性能承诺。真正使用时应记录输入图片、问题、Provider、VEP 长度、总耗时和任务是否完成,再判断压缩是否值得。

项目地址:lora-sys/free-vision-skill ↗。许可证为 MIT。

阅读完成 · 回笔记列表

评论

评论公开保存在 GitHub Discussions。

正在准备评论区…

评论框就绪后可以使用 GitHub 登录参与讨论,也可以直接打开仓库 Discussions。 打开 GitHub Discussions ↗

评论会发布到 lora-sys/loraSys 仓库的公开 Discussions。

其他联系方式