Compare commits
9 Commits
2456ad983c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f396ed32a | |||
| d81924b13b | |||
| 54f4d9065c | |||
| 1d974536e4 | |||
| 920b299af2 | |||
| 952b9f0816 | |||
| b4c5e2da5f | |||
| cd7575fd28 | |||
| b18a488030 |
@@ -0,0 +1,128 @@
|
||||
# nanobot 图片识别修复补丁
|
||||
|
||||
> 适用对象:`nanobot`(uv 工具安装,位于 `~/.local/share/uv/tools/nanobot-ai/`)
|
||||
> 修复日期:2026-08-21
|
||||
> 备份目录:本目录 `/home/ubuntu/nanocode/nanobot-patches/`
|
||||
|
||||
---
|
||||
|
||||
## 一、问题现象
|
||||
|
||||
使用支持视觉的模型(如 `deepseek-v4-flash-vision-exp`)时,通过 `read_file` 读取图片后,模型**看不到图片**,只收到类似 `(Image file: xxx.png)` 的占位符文本,导致无法识别图片内容。
|
||||
|
||||
## 二、根因
|
||||
|
||||
`nanobot/providers/openai_compat_provider.py` 的 `_sanitize_messages()` 中有一行逻辑:
|
||||
|
||||
```python
|
||||
force_string_content = bool(self._spec and self._spec.name == "deepseek")
|
||||
```
|
||||
|
||||
只要 provider 名字是 `deepseek`,它就把**所有**消息的 `content` 强制转为纯字符串。而 `content` 一旦是字符串,里面 `image_url` / `input_image` / `image` / `file` 等视觉块就会被剥离,图片信息丢失。
|
||||
|
||||
> 该逻辑本意是兼容 DeepSeek 纯文本模型的"只收字符串 content",但**一刀切**地也作用到了视觉模型上,导致视觉内容被丢弃。
|
||||
|
||||
## 三、修复方法
|
||||
|
||||
在 `openai_compat_provider.py` 中:
|
||||
|
||||
1. 新增一个静态方法,判断 `content` 是否包含视觉块:
|
||||
|
||||
```python
|
||||
@staticmethod
|
||||
def _content_has_vision_blocks(content: Any) -> bool:
|
||||
"""Return True when content is a block list containing image/file blocks.
|
||||
|
||||
Used to keep vision content intact for multimodal models (e.g.
|
||||
``deepseek-v4-flash-vision-exp``) instead of coercing it to plain text.
|
||||
"""
|
||||
if not isinstance(content, list):
|
||||
return False
|
||||
for item in content:
|
||||
mapping = OpenAICompatProvider._maybe_mapping(item)
|
||||
if not mapping:
|
||||
continue
|
||||
if mapping.get("type") in ("image_url", "input_image", "image", "file"):
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
2. 在 `_sanitize_messages()` 的强制转字符串条件里,增加"含视觉块则跳过"的判断:
|
||||
|
||||
```python
|
||||
if (
|
||||
force_string_content
|
||||
and not (clean.get("role") == "assistant" and clean.get("tool_calls"))
|
||||
and not OpenAICompatProvider._content_has_vision_blocks(clean.get("content"))
|
||||
):
|
||||
clean["content"] = self._coerce_content_to_string(clean.get("content"))
|
||||
```
|
||||
|
||||
> 关键点:`_content_has_vision_blocks()` 判断在 `_coerce_content_to_string()` **之前**生效,视觉块因此被保留。纯文本消息仍会走原字符串转换逻辑,不受影响。
|
||||
|
||||
## 四、涉及文件与路径
|
||||
|
||||
- 被修改文件:
|
||||
`/home/ubuntu/.local/share/uv/tools/nanobot-ai/lib/python3.11/site-packages/nanobot/providers/openai_compat_provider.py`
|
||||
|
||||
- 本目录备份:
|
||||
- `openai_compat_provider.py.before_patch` —— 修复**前**(旧)文件
|
||||
- `openai_compat_provider.py.after_patch` —— 修复**后**(当前)文件
|
||||
|
||||
## 五、⚠️ 重要:uv 升级会覆盖补丁
|
||||
|
||||
`nanobot-ai` 是通过 `uv tool` 安装的。一旦执行升级(例如 `uv tool upgrade nanobot-ai` 或重新安装),`site-packages` 下的文件会被**覆盖**,补丁随之丢失。
|
||||
|
||||
**重新打补丁的方法**(升级后执行):
|
||||
|
||||
```bash
|
||||
# 1. 确认当前文件与 before_patch 是否一致(若一致说明补丁已丢失)
|
||||
diff /home/ubuntu/nanocode/nanobot-patches/openai_compat_provider.py.before_patch \
|
||||
/home/ubuntu/.local/share/uv/tools/nanobot-ai/lib/python3.11/site-packages/nanobot/providers/openai_compat_provider.py
|
||||
|
||||
# 2. 若补丁丢失,用 after_patch 覆盖回去
|
||||
cp /home/ubuntu/nanocode/nanobot-patches/openai_compat_provider.py.after_patch \
|
||||
/home/ubuntu/.local/share/uv/tools/nanobot-ai/lib/python3.11/site-packages/nanobot/providers/openai_compat_provider.py
|
||||
|
||||
# 3. 重启 nanobot 服务使补丁生效
|
||||
sudo systemctl restart nanobot
|
||||
```
|
||||
|
||||
## 六、验证方法
|
||||
|
||||
用 nanobot 自身的 venv Python 跑一次完整链路(构造图片块 → `_sanitize_messages` → `chat`),确认 `content` 仍为 `list` 且保留 `image_url` 块,并能返回图片描述:
|
||||
|
||||
```bash
|
||||
PY=/home/ubuntu/.local/share/uv/tools/nanobot-ai/bin/python
|
||||
$PY -c "
|
||||
import asyncio, base64
|
||||
from nanobot.providers.registry import PROVIDERS
|
||||
from nanobot.providers.openai_compat_provider import OpenAICompatProvider
|
||||
|
||||
spec = next(s for s in PROVIDERS if s.name=='deepseek')
|
||||
key = 'sk-xxxx' # 替换为 DeepSeek API Key
|
||||
model = 'deepseek-v4-flash-vision-exp'
|
||||
b64 = base64.b64encode(open('/path/to/img.png','rb').read()).decode()
|
||||
messages = [{'role':'user','content':[
|
||||
{'type':'image_url','image_url':{'url':'data:image/png;base64,'+b64}},
|
||||
{'type':'text','text':'请描述这张图片'}
|
||||
]}]
|
||||
prov = OpenAICompatProvider(api_key=key, spec=spec, default_model=model)
|
||||
async def main():
|
||||
s = prov._sanitize_messages(messages)
|
||||
print('blocks:', [b.get('type') for b in s[0]['content']]) # 期望 ['image_url','text']
|
||||
resp = await prov.chat(messages, model=model)
|
||||
print(resp.text)
|
||||
asyncio.run(main())
|
||||
"
|
||||
```
|
||||
|
||||
## 七、关于"识别能力"的说明
|
||||
|
||||
- **图片识别能力本身**是模型自带的(如 `deepseek-v4-flash-vision-exp`、Kimi 等视觉模型都支持),**不需要**记忆到 nanobot 的长期记忆里。
|
||||
- **需要防丢失的**是"代码补丁"这一工程事实:它属于 `site-packages` 的源码改动,会随 `uv` 升级被覆盖。因此用**文档 + 前后备份**来保护,而不是当作模型能力去记忆。
|
||||
|
||||
## 八、关联历史
|
||||
|
||||
- 修改前备份文件:`/home/ubuntu/backup_openai_compat_provider.py.1787316174`
|
||||
- 历史记忆(memory)中的补丁位置与"uv 升级会覆盖、需重新打"的记录与此文档一致。
|
||||
|
After Width: | Height: | Size: 702 KiB |
|
After Width: | Height: | Size: 982 KiB |
|
After Width: | Height: | Size: 628 KiB |
|
After Width: | Height: | Size: 825 KiB |
|
After Width: | Height: | Size: 603 KiB |
|
After Width: | Height: | Size: 702 KiB |
|
After Width: | Height: | Size: 982 KiB |
|
After Width: | Height: | Size: 628 KiB |
|
After Width: | Height: | Size: 825 KiB |
|
After Width: | Height: | Size: 603 KiB |
|
After Width: | Height: | Size: 962 KiB |
|
After Width: | Height: | Size: 363 KiB |
|
After Width: | Height: | Size: 949 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 949 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 182 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 289 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 347 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 130 KiB |