Skip to content

[Bug] Content between embedder limit and chat_window_max_tokens is silently stored without a vector | 内容落在嵌入上限与切分阈值之间时静默丢失向量 #2461

Open

Description

@KongFUN2018

Pre-submission checklist | 提交前检查

  • I have searched existing issues and this hasn't been mentioned before | 我已搜索现有问题,确认此问题尚未被提及
  • I have read the project documentation and confirmed this issue doesn't already exist | 我已阅读项目文档并确认此问题尚未存在
  • This issue is specific to MemOS and not a general software issue | 该问题是针对 MemOS 的,而不是一般软件问题

Bug Description | 问题描述

当单条记忆内容的 token 数处于 (embedder 单条上限, chat_window_max_tokens] 区间时,/product/add 返回成功(code=200,有 memory_id),记忆也能在 /product/get_memory 中查到,但该条记忆没有向量(metadata.vector_sync="failed"、无 metadata.embedding),任何语义搜索都永远搜不到它。全程无错误上报,属于静默数据损坏。

对照观察:

  • 内容 < embedder 上限:正常入库、可搜索
  • 内容 > chat_window_max_tokens:触发 _split_large_memory_item(),切块后正常入库、可搜索
  • 内容落在两者之间:既不切块、也嵌不进去 → 静默无向量

How to Reproduce | 如何重现

环境前提:embedder 单条上限小于 chat_window_max_tokens(默认 1024)。例如本地 bge-m3 via llama.cpp llama-server(我们部署下单条超过约 512 token 返回错误,实测约 500+ 中文字符触发)。

  1. /product/add 一条约 700 token 的中文长文(> 512 且 < 1024),async_mode="sync"
  2. add 正常返回 code=200、有 memory_id
  3. /product/get_memory 能查到该记忆,但 metadata.vector_sync == "failed"、无 metadata.embedding
  4. /product/search 用任意 query 均无法召回该记忆

Environment | 环境信息

  • MemOS version: 部署于 v2.0.31;复核 main 最新(v2.0.33)后问题仍存在
  • Python version: 3.11.16
  • Operating System: Ubuntu (WSL2), docker compose 部署
  • Embedder: 本地 bge-m3 (llama.cpp llama-server)

Additional Context | 其他信息

根因分析(两个因素叠加):

  1. 分段阈值与嵌入上限脱节:chat_window_max_tokens(simple_struct.py 默认 1024,api/config.py 两处 mem_reader 装配均未显式传参)同时是 _split_large_memory_item() 的触发阈值——item_tokens <= max_tokens 时直接返回原 item 不切分。而嵌入端对超过其单条上限的输入直接报错。于是 (上限, 1024] 区间的内容既不切分也嵌不进去。
  2. 切分器对中文长段落失效:chunker.backend="sentence"(chonkie)不识别中文句号,长中文段落即使触发切分也可能整段穿过——切了等于没切。

补充:v2.0.33 新增的 [EMBED_FALLBACK](批量失败后逐条重试)能避免"一条超长拖垮整批",但超长的那条单条重试仍然失败,无法挽救本条记忆的向量。

影响范围:所有 embedder 单条上限低于 chat_window_max_tokens 的部署(本地小批量 embedder 很常见)。本地实例曾因此累计 22 条记忆无向量,且事前无任何告警。

修复建议(我们本地已验证过,如认可方向可整理为 PR):

  1. mem_reader 装配时把 chat_window_max_tokens 暴露为可配置(env 可调),默认值不高于 embedder 单条上限
  2. _split_large_memory_item() 在 chunker 输出后增加兜底:对仍超出预算的 chunk 做 token 计数的硬切(二分字符窗、优先标点切),保证交给 embedder 的每块必不超限

本地实测:1170 字中文长文 → 切为 2 块、全部带向量、可正常搜索。

Willingness to Implement | 实现意愿

  • I'm willing to implement this myself | 我愿意自己解决

Activity

  1. added
    ai:taskDispatched to AI coding agent | 已派发给 AI 编码任务
    area:coreMOS 编排层 / 框架底座 / 跨模块问题
    status:in-progressSomeone or AI is working on it | 人工或 AI 正在处理
    types:bugSomething isn't working | 功能异常
    on Oct 8, 2026
  2. self-assigned this
    on Oct 8, 2026
  3. Memtensor-AI commented on Oct 8, 2026

    @Memtensor-AI
    Collaborator

    🤖 AutoDev has picked up this issue and started working on it.

    Task ID: 00c0f8f3534623df
    Working branch: bugfix/autodev-2461-20261008063231554
    Target branch: latest dev* branch
    Workflow: opsp (analysis → coding → testing → PR)

    I will post the PR link here once done. If I need more information, I will ask in the comments.

  4. KongFUN2018 commented on Oct 8, 2026

    @KongFUN2018
    Author

    补充:我们已在本地 v2.0.33 上实现了这个修复,并做了判别性验证,实现细节供参考(如需完整 patch 可提供)。

    修复方案(两层,缺一不可)

    第 1 层——装配时把分段阈值压到嵌入上限之下(src/memos/api/config.py,两处 mem_reader 装配的 embedder 之后):

    "chat_window_max_tokens": int(
        os.getenv("MEM_READER_CHAT_WINDOW_MAX_TOKENS", "480")
    ),

    默认 480:本地 embedder 单条上限 512,留约 6% 余量;env 可调。

    第 2 层——chunker 输出后加硬切兜底(multi_modal_struct.py 的 _split_large_memory_item,chunks = self.chunker.chunk(item_text) 之后、chunk_total = len(chunks) 之前):

    budget = max(128, int(max_tokens * 0.85))
    normalized = []
    for chunk in chunks:
        chunk_text = chunk.text if hasattr(chunk, "text") else chunk
        if chunk_text and self._count_tokens(chunk_text) > budget:
            normalized.extend(self._hard_split_text(chunk_text, budget))
        else:
            normalized.append(chunk)
    chunks = normalized
    _hard_split_text 实现(二分字符窗 + 标点优先硬切)
    def _hard_split_text(self, text: str, budget_tokens: int) -> list[str]:
        """Split text by binary-searched character windows so every piece counts
        <= budget_tokens under self._count_tokens (cut preferring punctuation)."""
        pieces = []
        start, n = 0, len(text)
        while start < n:
            lo, hi, best = start + 1, n, start + 1
            while lo <= hi:
                mid = (lo + hi) // 2
                if self._count_tokens(text[start:mid]) <= budget_tokens:
                    best = mid
                    lo = mid + 1
                else:
                    hi = mid - 1
            end = best
            window_start = end - max(1, (end - start) // 5)
            for i in range(end - 1, window_start - 1, -1):
                if text[i] in "。!?;!?;\n":
                    end = i + 1
                    break
            piece = text[start:end].strip()
            if piece:
                pieces.append(piece)
            start = end
        return pieces

    为什么两层都要:只调阈值——分句器失明时(不识别中文标点、整段穿过)仍可能产出超限单块;只做硬切——(嵌入上限, chat_window_max_tokens] 区间根本不触发 _split_large_memory_item,硬切没有机会执行。

    判别性验证(v2.0.33 实测)

    注意:直接写长文本测试不能判别修复是否生效——LLM 抽取会把长内容压缩成短记忆,走不到切块路径(修复前后都会"通过")。需要 stub 掉 chunker 构造"分句器整段穿过"的场景:

    版本 1207 字(1035 token)输入结果
    上游 a7367d0 1 块 / 1035 token → 超嵌入上限,静默无向量
    修复后 3 块 / 最大 401 token → 全部 vector_sync=success、可搜索

    另:本机曾因此累计 22 条记忆无向量(事后对照元数据逐条重灌恢复),问题真实且全程无任何错误上报。

  5. Memtensor-AI commented on Oct 8, 2026

    @Memtensor-AI
    Collaborator

    ✅ AutoDev task 00c0f8f3534623df completed.

    Summary: Fixed the silent no-vector write for memory items whose token count falls in (embedder per-item limit, chat_window_max_tokens] (issue #2461). Such an item was previously neither split (the splitter only fired above chat_window_max_tokens, hard-coded to 1024 and never passed by either api/config.py mem_reader assembly site) nor embeddable (it exceeded the embedder's limit). /product/add still returned 200, but the item was persisted with metadata.embedding=None and vector_sync != "success", and search_by_embedding filters on vector_sync == "success" — so the memory was permanently invisible to semantic search. A secondary cause: _split_large_memory_item trusted chunker output and never re-checked the token budget per chunk, and the sentence chunker (chonkie) does not split CJK punctuation (it can also raise on an incompatible installed version), in which case the except branch returned the over-budget item unchanged.

    The fix adds an explicit, env-configurable chat_window_max_tokens field on BaseMemReaderConfig (MEM_READER_CHAT_WINDOW_MAX_TOKENS, default 1024 so existing deployments are unchanged), wired into all three mem_reader assembly sites via APIConfig.get_chat_window_max_tokens() with fault-tolerant parsing. The effective split budget is now min(chat_window_max_tokens, embedder per-item limit). A new _chunk_within_budget helper re-checks every chunker-produced chunk and hard-splits any that still exceeds the budget — preferring a punctuation boundary (CJK and Latin terminators) and falling back to a binary-search character window — and a chunker that raises or yields nothing now hard-splits instead of passing the over-budget item through. _concat_multi_modal_memories uses the effective budget for both the split trigger and the window accumulator, plus a guard that emits over-budget items directly rather than aggregating them into a larger window. Finally, the per-item embedding retry (and the base reader's _make_memory_item path) truncates to budget before calling the embedder, so one over-limit item can no longer leave a whole batch without vectors.

    Verification: a new 18-case regression suite (tests/mem_reader/test_embed_budget_split.py) passes, covering budget derivation, punctuation-free CJK splitting within budget, boundary preference, empty/failed chunker fallback, contiguous re-indexing, the 700-token in-window case, and the mixed-batch embedding fallback. Affected suites (mem_reader, configs, api, memories, mem_cube) report 341 passed with 4 pre-existing failures confirmed on the untouched baseline (markitdown extra not installed; unrelated kv-cache API). ruff check and ruff format both clean. An end-to-end reproduction using the real SentenceChunker and tiktoken counter with a 512-token embedder limit turns the issue's 780-token Chinese paragraph into chunks of 512 + 268 tokens, all embedded, while the same script's pre-fix branch reproduces the reported embedded=[False].

    Out of scope: backfilling vectors for memories already stored without one.

    Base branch: main
    Branch: bugfix/autodev-2461-20261008063231554
    Commit: c3572b0
    PR: #2462
    Assigned to: @syzsunshine219
    Reviewers: @WeiminLee

  6. added
    ai:testingAI agent is running tests | AI 正在运行测试
    and removed
    ai:codingAI agent is coding | AI 正在编码
    on Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

ai:taskDispatched to AI coding agent | 已派发给 AI 编码任务ai:testingAI agent is running tests | AI 正在运行测试area:coreMOS 编排层 / 框架底座 / 跨模块问题status:in-progressSomeone or AI is working on it | 人工或 AI 正在处理types:bugSomething isn't working | 功能异常

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions