Skip to content

[Bug] Markdown 导入将空正文/无 choices 当成合法空提取,无告警显示 completed(main/testing 均可复现) #134

Closed

Description

@Iris3971

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 存在:

  1. dehydrator.py:无 choices 返回空字符串;空 content 也归一为空字符串。
  2. import_memory.py:空白 raw 直接返回空列表,不经过 JSON 校验。
  3. 空 items 返回 True,被视为分块成功。
  4. _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

  1. added a commit that references this issue on Nov 5, 2026
  2. P0luz commented on Nov 5, 2026

    @P0luz
    Owner

    已修复:3ff8544 。Markdown 导入时,模型返回空白(无 choices、content 为 null、空串、仅空白、finish_reason=length 且正文为空)不再当成合法空提取,该分块计为失败,并在错误里记录分块序号;任务不会再显示零失败的 completed。模型明确返回 [] 仍视为成功。新增 8 项回归,修复前均失败。修复目前在 testing 分支,尚未合入 main 正式版;新版本仍能复现请重新打开。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions