Repository navigation
Description
Markdown 导入收到模型的空正文(含 null/空白)或无 choices 响应时,最终仍显示 completed、memories_created=0、chunks_failed=0、errors=[]。这些异常响应与模型明确返回合法空数组 [] 的正常“无值得记忆的内容”结果无法区分。
这不是要求每次导入必须创建记忆;合法 [] 应继续被允许。问题是缺少可解析的 extraction 响应也被静默当作成功。
已核对的版本与复现范围
| 版本 | 固定提交 | extraction 请求预算 |
|---|---|---|
| main / 3.6.14 | 115d831d979bf67efc7150f976e2cb34c333c935 |
2048 |
| testing / 3.8.0 候选 | 32ecb932d8fa777a0424babfdb28be19ab84fbeb |
配置的 4096 |
| 当前 testing / 4.0.0 | 4ec562e4f3fa897e6b93fd09eb9f31e8e7f7cba5 |
配置的 4096 |
在本地 Python 3.13.9 使用上述固定源码的原始函数体做了离线受控复现:通过 AST 原样加载 Dehydrator._chat_once()、ImportEngine._extract_memories()、_parse_extraction()、_process_single_chunk()、_process_chunks() 及必要的原始工具函数;仅替换供应商响应、状态持久化、去重准备与桶写入。不发起网络请求,不访问真实 vault。
共执行 3 个版本 × 9 种响应情形,27 个断言全部符合下表。此处确认的是生产函数的受控行为,不声称已在本机完成完整 Docker/真实供应商端到端复现。
实际结果
三个版本行为一致:
| 模型响应情形 | 最终状态 | 创建数 | failed chunks | state.errors | WARNING |
|---|---|---|---|---|---|
choices=[] |
completed | 0 | 0 | [] | 0 |
message.content=null |
completed | 0 | 0 | [] | 0 |
message.content="" 或纯空白 |
completed | 0 | 0 | [] | 0 |
| 只有 reasoning_content,无 content 正文 | completed | 0 | 0 | [] | 0 |
明确返回合法 [](正常对照) |
completed | 0 | 0 | [] | 0 |
非法 JSON [(失败对照) |
error | 0 | 1 | 非空 | 2 |
| 合法一条记忆(成功对照) | completed | 1 | 0 | [] | 0 |
finish_reason="length" 且正文为空 |
completed | 0 | 0 | [] | 1 |
最后一行虽然有截断日志,但失败计数和任务错误仍为空。
代码路径
以下链接固定到 main / 3.6.14,说明该行为早于 PR #130 存在:
- dehydrator.py:无 choices 返回空字符串;空 content 也归一为空字符串。
- import_memory.py:空白 raw 直接返回空列表,不经过 JSON 校验。
- 空 items 返回 True,被视为分块成功。
_process_chunks()在chunks_failed == 0时将任务标为completed。
对照 3.6.14 与 3.8.0 候选:extraction prompt 和 _process_single_chunk() 原样一致;_extract_memories() 仅替换了预算参数。因此本 issue 不将该行为归因为 #130 的回归,也不将 >2048-token 验收未覆盖当成导入故障。
最小复现
在已安装项目依赖的 checkout 中,将下段保存为 /tmp/repro_empty_import.py,执行:
PYTHONPATH=src python /tmp/repro_empty_import.py这段只替换 LLM 返回值和 I/O,实际执行原来的 extraction/parse/chunk 状态处理。上述离线 AST harness 也核对了该复现片段在三个版本的结果。
最小复现脚本
"""Run in an installed Ombre Brain checkout: PYTHONPATH=src python /tmp/repro_empty_import.py
Only the LLM and state/bucket I/O are replaced. No network or real vault is used.
"""
import asyncio
import json
from types import SimpleNamespace
from unittest.mock import AsyncMock
from import_memory import ImportEngine
async def replay(raw):
engine = object.__new__(ImportEngine)
engine.config = {"human": "用户"}
engine.dehydrator = SimpleNamespace(
api_available=True, import_max_tokens=4096,
_chat=AsyncMock(return_value=raw),
)
data = {
"status": "running", "total_chunks": 1, "processed": 0,
"api_calls": 0, "memories_created": 0, "memories_merged": 0,
"memories_skipped": 0, "memories_raw": 0,
"chunks_failed": 0, "errors": [],
}
engine.state = SimpleNamespace(
data=data, save=lambda: None, to_dict=lambda: dict(data),
)
engine._chunks = [{
"content": "[用户] 周三我参加了北桥读书会,下周会带上蓝色封面的小说。"
}]
engine._paused = False
engine._prime_exact_content_hashes = AsyncMock()
engine._create_import_item_if_new = AsyncMock(return_value=True)
result = await engine._process_chunks(False)
return {
key: result[key] for key in (
"status", "processed", "api_calls", "memories_created",
"chunks_failed", "errors",
)
}
async def main():
cases = {
"empty_body": "",
"valid_empty_array": "[]",
"invalid_json": "[",
"valid_one_memory": json.dumps(
[{"name": "读书会", "content": "周三参加读书会,下周带蓝色小说。"}],
ensure_ascii=False,
),
}
for name, raw in cases.items():
print(name, json.dumps(await replay(raw), ensure_ascii=False))
if __name__ == "__main__":
asyncio.run(main())首两种情形均输出:
{"status":"completed","processed":1,"api_calls":1,"memories_created":0,"chunks_failed":0,"errors":[]}预期与建议修复边界
- 区分“缺少有效模型响应”和“模型明确返回合法空数组”;前者进入可见失败/诊断路径,后者可以完成并提示本块没有提取到记忆。
- 对无 choices、空 content、仅 reasoning 无 JSON,以及已明确 length 截断的空响应,至少提供脱敏的错误类型与 chunk 索引,避免用户将“流程结束”误认为“输入中的记忆已经保存”。
- 不要求默认记录原始提示词/响应正文。若补充诊断,可使用响应字符数、finish_reason、token 用量(区分 reasoning)、trace ID 等非正文元数据,并按项目隐私边界实现。
- 增加对应回归测试:异常空响应不能与合法
[]同态;合法空结果、合法记忆和非法 JSON 的既有语义保持清楚。
社区现场的证据边界
本次调查由一份纯合成 Markdown 的现场结果触发:10 turns、单 chunk、报告一次模型调用,计费输出约 1 token,任务 completed 但创建 0 条记忆。已提供材料的原文、解析 turns、chunk 逐项一致,5 组各 18 项合成事实完整进入 chunk。
该现场未保留原始模型响应,因此不能确认它返回了 []、空 content 或其他结果。材料反复强调“虚构/合成”,可能影响模型的筛选判断,但尚未进行供应商对照验证;这只是线索,不作为本 issue 的根因结论。也不据此推断真实历史数据丢失。
本 issue 报告的是上面已独立受控复现的异常空响应处理与诊断缺口。无需用户继续反复压测即可复现。未附带任何真实记忆、凭据或个人配置。
Activity
fix: 导入空响应计为失败分块(#134);grow 短内容打标失败仍保存原文(#139)
P0luz commentedon Nov 5, 2026
已修复:3ff8544 。Markdown 导入时,模型返回空白(无 choices、content 为 null、空串、仅空白、finish_reason=length 且正文为空)不再当成合法空提取,该分块计为失败,并在错误里记录分块序号;任务不会再显示零失败的 completed。模型明确返回
[]仍视为成功。新增 8 项回归,修复前均失败。修复目前在 testing 分支,尚未合入 main 正式版;新版本仍能复现请重新打开。