Skip to content

standalone /context 缺少档位信号:嵌入降级后调用方无从感知 #383

Closed

Description

@modusensus

要解决的问题 / Problem

GET /context(#370 落地、0.8.14 发布)在查询嵌入不可用时会静默降级:embedder 未注入(createStandaloneApi 的可选参数,宿主与 dsh-mneme-serve 各传 semantic 装配的同一实例),或嵌入超时(withEmbedTimeout,默认 3000ms)时,端点按「规则 + BM25」档返回。响应仍是 200,形状与语义档完全一致——{profile, rules, items, pinnedCount} 里没有任何字段能区分这两档。

GET /search 有对应信号:回 mode: "vector" | "keyword"。

影响面:以 /context 为唯一注入来源的第三方(如 Mneme-Bridge 的 remote 模式)无法感知自己长期跑在降级档上——它的回落逻辑只在 404 / 网络失败 / 响应形状不符时触发,静默降级不触发。对外表现是「检索质量莫名下降」而非报错,排查成本落在调用方。这个缺口在 #363 的接入评审里由 Mneme-Bridge 侧提出。

期望的方案 / Proposed solution

/context 响应增加一个档位字段(命名待定:与 /search 对齐用 mode,或独立的 tier / degraded):

  • 纯增量:现有四个键的语义与形状不动,存量调用方零感知;
  • 语义档(嵌入成功)与规则档(embedder 缺失、嵌入超时或失败)可区分;
  • 具体命名与取值形态在实现 PR 里定。

考虑过的替代方案 / Alternatives considered

  1. 不加字段,调用方自行探测 embedder 可用性——嵌入超时是运行时事件、无法预知,且 daemon 与宿主是不同进程,做不到;
  2. 降级时返回非 200——破坏现有契约:规则档是合法结果不是错误,/context 的既有行为明确「嵌入失败绝不 500」;
  3. 用 /health 类端点暴露 embedder 状态——反映不了单次请求的超时,粒度不够。

实现口径确认 / Implementation gates

本项是响应字段增量,不涉及新配置键与 schema 变更;若要加开启开关,则按 opt-in 默认关注册进 settings 白名单。验收含回归测试:dsh-mneme/test/standalone-api.test.js:592 的响应形状锁(当前锁四键)需同步,并补两档的档位用例(embedder 注入 / 缺省 / 嵌入超时三条路)。

Activity

  1. modusensus commented on Oct 20, 2026

    @modusensus
    MemberAuthor

    现状(2026-10-21 对照 main 实测)

    没做,/context 的响应里仍然没有任何档位信号。

    dsh-mneme/src/api-standalone.js 的 /context 处理器(约 :679-684)当前返回四键:

    { profile, rules, items, pinnedCount }

    :683 是 pinnedCount: pinnedStats.shown ?? 0。没有 mode、没有 tier、没有 degraded。

    为什么这条比看起来重要

    嵌入降级(向量不可用、退化成纯 BM25)在调用方视角是完全静默的:拿到的 items 看起来正常,只是召回质量悄悄变差。外部集成方无法据此降级自己的期望或打日志。

    还缺什么

    1. 响应加一个档位字段(建议 tier: "full" | "degraded" | "unknown"),fail-closed:判不出来就报 unknown,不要报 full;
    2. 降级原因可枚举(无 embedder / 维度不符 / 索引未就绪),与 /health 的既有三态口径保持同源;
    3. 加一条形状锁用例,断言四键变五键且 tier 必填。

    接手建议

    改动面很小(一个处理器 + 一个测试)。/health 已经在报 embeddingDimensionBreakdown(2e45040),数据源是现成的,不需要新算。

  2. z2Ace0107 commented on Oct 23, 2026

    @z2Ace0107
    Contributor

    想接这条,按你建议的口径做:

    1. src/api-standalone.js 的 /context 处理器加 tier 字段,取值 full / degraded / unknown,fail-closed——判不出来报 unknown,不报 full;
    2. 降级原因枚举(无 embedder / 维度不符 / 索引未就绪),与 /health 的三态口径同源;
    3. 补形状锁用例:断言四键变五键且 tier 必填。

    纯增量,现有四键的语义和形状不动,存量调用方零感知;不加配置键。数据源用 /health 已经在算的 embeddingDimensionBreakdown,不重算。

    我同时认了 #461 和 #421,按由易到难串行做(#461 → #383 → #421),两周内都交 PR。

  3. modusensus commented on Nov 2, 2026

    @modusensus
    MemberAuthor

    已在 0.9.5 交付,本 issue 关闭。

    落点

    • dsh-mneme/src/api-standalone.js:889-961:/context 响应新增第五键 tier,三值 full / degraded / unknown。
    • 取值判据:查询串为空 → unknown(语义路未启用,不宣称档位);查询向量非空 → full;否则 → degraded。
    • fail-closed:探测本身失败(库被锁 / 半开 / 旧装配没有那个方法)也保持 unknown,不报 full。把「不知道」写成「正常」正是这条 issue 要消灭的那种静默。

    实现口径与你提议的差异

    你建议用 /health 已在算的 embeddingDimensionBreakdown 作数据源、降级原因做进响应体。实际实现把原因留在 daemon stderr 的那行 warn 里,响应体只多 tier 一个键——理由是一个键已经够调用方决定「要不要重试或提示」,而把原因枚举也放进去会多出一份需要跟着 /health 三态口径同步维护的枚举。tier 的语义与 /search 的 mode 是两回事(前者描述嵌入通道,后者描述检索算法),所以没有复用那个名字。

    存量四个键的语义与形状一个字节没动,没加任何配置键。

    @z2Ace0107 你的 PR #508 与后续评审补丁 #513 都合进 main 了,CHANGELOG 的 0.9.5 节把 /context 的 tier、/memory/provenance 与两处 M.O. 去重键修复一起记在你名下。谢谢你——尤其 tier 那份「判不出来报 unknown 不报 full」的取舍,是这条 issue 真正的落点。


    顺带一条你可能想知道的:#508 与 #507 合并时都没带 Fixes #N 关键字,所以代码进了 main、issue 一直挂在 open。这不影响任何东西,只是我们这边收口的习惯——以后带一句 Fixes #383 就能自动关。

    如果对 unknown 这一档在你们的调用方那边够不够用还有想法,随时 reopen 或另开一条,我们接着改。

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions