mcpField
L
loci

loci

本地服务
github.com/3M1RY33T记忆与知识17 周前更新

关于 loci

loci 是面向编码 Agent 的分范围记忆工具,会将问题路由到相关项目,而非搜索合并后的语料。它结合代码结构与事件记录;当项目范围不明确时,可以拒绝作答而不是猜测。

主要功能

  1. 01在不同项目范围间路由问题
  2. 02搜索代码结构和事件记录
  3. 03返回带引用的答案
  4. 04拒答并列出候选项目
  5. 05索引仓库并可选使用嵌入

适用场景

  1. 01了解当前项目中某项功能的实现
  2. 02回忆过去某次改动的原因
  3. 03查找使用指定工具或库的项目
  4. 04范围不明确时避免跨项目作答

MCP 相关

可选的 `mcp` 安装附加项会加入 MCP 服务;输入未说明其提供的工具。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"3m1ry33t-loci": {
"command": "uvx",
"args": [
"loci-mem"
]
}
}
}
启动命令uvx loci-mem

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
pypiloci-mem0.6.0

工具

3

收录的工具列表 · 更新于 2 周前

参数类型必填说明
kinteger否每个范围的 Episode 命中数。
cwdstring否用户的工作目录。只要已知就传入:这是最重要的路由信号。
groupstring否将范围限制为一个项目组,例如 'me' 或 'client:delroy'。
scopestring否按名称指定项目,绕过路由。
rerankboolean否交叉编码器重排序;速度较慢,但排序效果更好。
questionstring是

详细介绍

loci

面向编码 Agent 的范围化记忆。 一个位于两个存储前面的路由器。

question ──▶ router ──▶ ┌── structure store   what calls what
             (no LLM)   └── episode store     what happened and why
                        └─▶ merged, cited answer   │
                            or ABSTAIN ────────────┘

知识图谱保存结构,但不保存散文,因此无法回答 为什么 Cookie 被丢弃了。逐字回忆系统保存散文,但没有调用图,因此无法回答 什么调用了 run_agent_turn。两者都要求你在写入时指定命名空间。

loci 在读取时决定范围。一条提问路径既能回答 “这里的身份验证是如何工作的”,也能回答 “我在任何项目中解决过这个问题吗”——变化的只有范围集合的大小。


一图看懂整体结构

一条由独立商店组成的街道。每家商店代表一个项目——有自己的库存、自己的账本,不会汇总到共享仓库。每家都维护两本账:什么调用什么,以及发生了什么、为什么发生。读取时要么一起读取,要么都不读。

商店挂着彼此重叠的标签——你的、这个客户的、那个供应商的、这个单体仓库的。一个标签可以覆盖街道两端的商店,这就是为什么它们是标签,而不是层级结构。

你不会亲自走遍这条街。你会询问调度器,它只知道哪些商店存有不常见的词。它会带你去最多三家商店,或者告诉你这里都没有,并指出可能有的地方。

每家商店还挂着一块招牌,说明它是什么——仓库、软件包、命令——并维护一份它会引用什么的清单。把两者对照着读,街道就能回答我的哪些商店从另一家采购。无论问调度器多少次都无法得出这个答案:这个问题关乎两家商店之间的道路,而调度器只知道它们各自存了什么。

街道还有一处欠缺:商店内部没有过道,因此在测试夹具中找到的词,不会像在已发布代码中找到的同一个词那样计入结果。


解决的问题

把所有项目放进一个索引,最大的项目就会胜出,不管问题是什么。与十个真实仓库合并而成的图谱进行比较:

问题返回的相关节点
“为什么管理员会话 Cookie 在 localhost 上被丢弃了?”18%——31 个节点中有 19 个来自最大的项目
“用户点击‘另存为 ZIM’时会发生什么?”2%——62 个节点中有 61 个来自最大的项目
“保留的着陆步骤是如何工作的?”98%——而最大的项目恰好就是答案所在

第三行就是陷阱。当答案恰好位于最大的语料库中时,合并检索看起来效果出色;否则就会失效。这是同一种失败的两个方向,只不过只显现了其中一个。


安装

发行包名是 loci-mem;命令、导入名称和项目名称都是 loci。PyPI 上的 loci 是一个无关的异常值检测软件包,已于 2018 年停止维护——这与安装为 dateutil 的 python-dateutil 情况相同。

pipx install loci-mem                 # routing + lexical search, one dependency
pipx install 'loci-mem[all]'          # + graphify, local embeddings, MCP server

基础安装只会拉取 numpy,除此之外什么也不装。自 0.6.0 起,路由、BM25 和字符 n-gram 矩阵都由编译后的 Rust 扩展提供,因此不再需要 scikit-learn(以及它依赖的 scipy)、rank-bm25 和 joblib。没有 torch、没有向量数据库,也不会下载模型,除非你主动要求。

macOS、Linux 和 Windows 均提供 wheel;从 sdist 安装时会从源码构建,需要 Rust 工具链(1.75+)。

可选项添加内容
graphify支持约 18 种语言的代码符号提取
embeddings本地 bge-small 语义排序
rerank交叉编码器重排(可针对每次查询选择启用)
mcpMCP 服务

快速入门

pipx install 'loci-mem[all]'
loci setup             # scan, graph, index, embed, calibrate -- one pass
loci ask "why was the session cookie dropped on localhost?"

setup 只会询问它无法替你决定的事项:哪些目录存放你的项目、是否注册它发现但不属于你的代码仓库,以及是否愿意一次性下载模型以启用语义搜索。最后它会运行 doctor,这样它无法处理的问题会成为你最后看到的内容,而不是让你一周后才从错误的答案中发现问题。

当 stdin 不是终端时,每个提示都会采用默认值,因此可以在容器中或由 Agent 无人值守地运行。-y 在终端中也会执行相同操作,--no-graphs / --no-embed / --no-calibrate 则可以预先决定是否执行各个步骤。

也可以手动执行。顺序取决于依赖关系,而非个人偏好:索引由图构建,索引会写入 embed 编码的分块,而 calibrate 则根据这些向量拟合语义下限。

loci scan ~/code       # register every git repo it finds, one scope each
loci graphs            # optional: add code symbols (free, no model calls)
loci index             # build the routing index + episode store
loci embed             # optional: local vectors for semantic recall
loci calibrate         # optional: fit routing thresholds to your corpus
loci doctor            # what is missing, and the command that fixes it

loci ask "which projects use wrangler and D1?"
loci eval              # measure routing accuracy on YOUR corpus

每个步骤都会写入下一步要读取的内容,因此顺序是固定的,而非可选的:

flowchart TD
    scan["loci scan ~/code<br/>writes ~/.loci/scopes.json"]
    graphs["loci graphs<br/>writes graphify-out/graph.json, one per scope"]
    index["loci index<br/>writes ~/.loci/episodes.json, scope_index.json, rankers/"]
    embed["loci embed<br/>writes ~/.loci/embeddings.npz"]
    calibrate["loci calibrate<br/>writes ~/.loci/calibration.json"]
    doctor["loci doctor<br/>writes nothing at all"]

    scan -->|"there is nothing to graph or index until a scope exists"| graphs
    graphs -->|"the index is built FROM graph.json; index first<br/>and every project routes on prose alone"| index
    index -->|"embed encodes episode chunks, and they do not<br/>exist until index has written the store"| embed
    embed -->|"calibrate fits a per-scope semantic floor from<br/>those vectors; with none it keeps a default"| calibrate
    calibrate -->|"and then reports whatever is still missing"| doctor

顺序错误地运行不会明显报错。它会生成一个可用的安装,但检索效果会悄然变差,而 doctor 正是用来指出这个问题的。

scan 会为每个 Git 代码仓库注册一个作用域。它还会从 Git 中读取每个代码仓库的所有者,显示各自的所有权,并在注册不属于你的代码仓库前征求同意;和其他提示一样,该提示会采用默认值,也就是注册所有代码仓库。

单体仓库也可以拆分为每个软件包一个作用域。在 loci scan 或 loci setup 中使用 --split,会将下一层中包含 package.json、pyproject.toml、Cargo.toml 或 go.mod 的目录纳入拆分;仓库本地的 .loci.json 则用于指定其他目录,且无论是否使用该标志都会生效。--split 默认关闭,原因是经过测量,而非出于谨慎:新作用域的别名包含其目录的原始名称,而别名的优先级高于当前工作目录(6.0 对 4.0)。在开发语料库中,将一个包含 glasses/ 的代码仓库拆分后,八个关于其他项目的手写问题被路由到了 Delroy/glasses——拆分前其中七个问题路由正确;将别名加成设为零后,八个问题中有六个恢复了正确路由。同一次运行中,Jekyll 的 _site/ 构建目录也成了一个独立作用域。你编写的 .loci.json 不会有这个问题:这些子项目是你刻意命名的,因此你知道它们叫什么。

ask 默认使用你的工作目录,而且理应如此。不提及项目的问题——“这是如何部署的?”、“如何运行测试?”——在提供 cwd 时,路由正确率为 100%。没有 cwd,这些问题就无法回答,而 loci 会对它们全部弃答,而不是猜测。此处 cwd 不是用于打破平局的信号,而是主要信号。


概念

范围。 一个项目,一个命名空间。已注册的 git 仓库、monorepo 中的一个包(通过 --split,或在 .loci.json 中命名),或任何你显式添加的内容。范围永远不会合并。

分组。 作用于范围的重叠标签:从 git 来源信息读取的 me 和 vendor:<org>;client:acme 及任何你手动声明的其他标签;以及 monorepo 自身的 ID,每个包和 monorepo 本身都会带上该 ID,因此 --group <monorepo> 可以触达没有被任何包认领的代码。一个范围可以属于多个分组,范围集合始终保持扁平:分组不会合并范围,也不会将一个范围嵌套在另一个范围中。

分组对问题的作用方式称为模式,模式回答两个不同的问题。通过你的工作目录触达时,explicit 不做任何处理,soft(默认值)会将每个外部范围的证据权重乘以 0.5,而 hard 会将路由限制在组内成员中,并在最佳答案位于组外时拒绝作答。通过命令行指定 --group X 时,三种模式都会将范围限制在 X 的成员中——模式只决定最佳答案位于组外时如何处理:hard 会拒绝作答并说明原因,soft 和 explicit 则会用组内的最佳答案作答。成员关系保存在范围注册表中,模式保存在 groups.json 中,因此重新扫描(会整体重写注册表)不会丢弃策略。实测:以 cwd 为锚点的 hard 模式会在问题提及外部项目时触发(12 个问题中 12 个),但在问题只包含该项目的词汇时不会触发(24 个问题中 0 个);因此,实际使用时,驱动它的是 --group 和项目名称,而不是你所在的位置。

结构存储。 谁调用了什么——符号、文件、引用,以及带有 file:line 引用的遍历。由 graphify 通过适配器提供。

事件存储。 发生了什么以及为什么——README 和文档、git 提交正文、文档字符串和注释块,以及你指定的任何笔记。内容按原样存储,按标题边界分块,并在写入前进行脱敏。

路由器。 以确定性方式决定问题属于哪些范围,不调用模型。信号按权重排序:显式项目名称(6.0)、工作目录(4.0)、词汇证据(实测为 0.1–1.5)、近期程度(0.15)。权重不代表实用性——cwd 是承载大多数实际问题的信号,因为大多数实际问题根本不会提及项目名称。

拒绝作答。 一种一等结果。ABSTAIN 表示询问用户,而不是选最大的那个。两个层级都可以拒绝。

只说不行的拒绝会把提问留给你,因此它会附上自己拒绝从中选择的候选列表——每个项目,以及使其入选的词语:

loci ask "how is caching handled?"

ABSTAINED - not enough of the question exists in any project.
  candidates: Delroy (handled, caching), odysseus (handled, caching), tensor-serve (caching)
  re-run with --scope <name>, or from inside the project directory.

一个范围只有在包含某个术语的语料不超过一半时,才有资格进入列表。仅仅包含该术语还不够:change、handled 和 work 几乎出现在每个项目中,因此匹配到它们只是巧合,而不是有力依据。在开发语料上的实测结果显示,这决定了系统是只列出三个项目,还是按得分顺序列出全部十四个;后者是注册表,而非候选列表。

如果没有任何项目达到这一门槛,就没有候选列表可供输出,此时如实说明就是答案——问题的主题没有在任何地方建立索引,选择一个范围只会把猜测转移到别处:

loci ask "xyzzy plugh frotz quuz"

ABSTAINED - not enough of the question exists in any project.
  no project holds a distinctive term from this question -- `loci doctor` shows what is not indexed.

架构

查询路径

本文件顶部的图是简要概览。完整流程如下:

loci ask "why was the session cookie dropped on localhost?"
  │
  │   --scope NAME jumps straight past routing to the fan-out, and drops the
  │   episode gate with it: that gate exists to stop an answer arriving from
  │   the wrong scope, and you have just named the right one.
  ▼
confinement                                                       groups.py
  │   reads the registry and groups.json. Never the index, never a model.
  │     --group X  ─▶  eligible = members of X, in ALL THREE modes
  │     else cwd   ─▶  the strictest group of the scope you are standing in
  ▼
route                                                             router.py
  │   reads scope_index.json: token ─▶ {scope: node_df}. One dict lookup per
  │   query token. Deterministic, sub-millisecond, no model call.
  │
  ├──▶ ABSTAIN — deictic, no_evidence or out_of_group. Names the cause, lists
  │             the scopes with a claim on the question and what each one
  │             holds, names the flag that fixes it, and queries nothing.
  │             An outcome, not an error.
  ▼
selected scopes, at most 3        one thread each; neither store is ever
  │                               queried across a scope boundary
  ├─── scope ─── scope ─── scope
  │      │
  │      │  expand the question against THIS scope's postings — a dict
  │      │  lookup, so a token the scope does not have cannot be invented —
  │      │  then append the tokens of its nearest embedded symbol labels
  │      │
  │      ├─ structure store   graphify query --graph <this scope's graph>
  │      │                    what calls what, with file:line citations
  │      │
  │      └─ episode store     BM25 + char 3–5 gram + embeddings, fused
  │                           gated: lexically grounded OR semantically
  │                           confident, so it can return nothing, and does
  ▼
merged, cited answer — one block per scope, and `no evidence in this scope`
wherever both of a scope's stores came back empty

两个阶段都会独立拒绝:路由器可以完全不选择范围,而它选中的范围也可能返回空结果。

路由决策

扇出之前的所有内容都是一个函数。它会为语料库中的每个范围评分,然后按固定顺序询问三个拒绝问题。别名或当前工作目录信号会跳过最后两个问题,但不会跳过第一个。

flowchart TD
    Q["question, plus cwd and --group when given"]
    CONF{"which group confines this question?"}
    E1["eligible = members of X<br/>strict only when that group's mode is hard"]
    E2["eligible = members<br/>strict"]
    E3["demoted = every scope outside the group"]
    E4["nothing confined, nothing demoted"]
    BASE["evidence base, for EVERY scope in the corpus<br/>sum(scope-idf x prominence) / query tokens / size^0.15"]
    PEN["demoted? base x 0.5<br/>here, before the boosts, so it cannot invert one"]
    BOOST["+ ALIAS 6.0, the question names the project<br/>+ CWD 4.0, DEEPEST containing scope only<br/>+ RECENCY 0.15, a tiebreak and no more"]
    TOPS["top = best ELIGIBLE scope<br/>top_all = best scope in the whole corpus"]
    OOG{"strict, and top_all is outside the group,<br/>and top_all would itself have been routable?"}
    A1(["ABSTAIN out_of_group<br/>the best answer is outside the group"])
    FORCED{"forced?<br/>top carries an alias hit or the cwd signal"}
    ENUM{"enumerative?<br/>which projects / what repos / any of my / where else / have I ever"}
    DEIX{"deictic?<br/>this / these / it / its / here / the project / the app / ..."}
    A2(["ABSTAIN deictic<br/>the question points at a subject it never names"])
    EV{"enough evidence?<br/>ANY ONE of the three is enough"}
    A3(["ABSTAIN no_evidence<br/>too little of the question exists in any project"])
    SEL{"did the question ask for a SET?"}
    SELECT["keep every eligible scope scoring at or above 0.85 x top,<br/>cap at 3, then force in every concentrated-token holder"]
    SETSEL["keep every eligible scope clearing 0.8 x the floor ON ITS OWN,<br/>in rank order, cap at 8 -- no ratio cutoff at all"]
    OUT(["selected scopes"])

    Q --> CONF
    CONF -->|"--group X, any mode"| E1
    CONF -->|"cwd scope's strictest group is hard"| E2
    CONF -->|"cwd scope's strictest group is soft"| E3
    CONF -->|"explicit, or no group at all"| E4
    E1 --> BASE
    E2 --> BASE
    E3 --> BASE
    E4 --> BASE
    BASE --> PEN
    PEN --> BOOST
    BOOST --> TOPS
    TOPS --> OOG
    OOG -->|yes| A1
    OOG -->|no| FORCED
    FORCED -->|yes| SEL
    FORCED -->|no| ENUM
    ENUM -->|"yes, and it is not picking ONE scope"| EV
    ENUM -->|no| DEIX
    DEIX -->|yes| A2
    DEIX -->|no| EV
    EV -->|"summed token evidence at or above the floor<br/>(7.6 shipped, refitted by loci calibrate)"| SEL
    EV -->|"at least 4 matched tokens"| SEL
    EV -->|"an eligible scope holds a CONCENTRATED token:<br/>held by at most 2 scopes, prominent inside them"| SEL
    EV -->|"none of the three"| A3
    SEL -->|no| SELECT
    SEL -->|yes| SETSEL
    SELECT --> OUT
    SETSEL --> OUT

这个结构中有四个关键点,而且都不显而易见。

forced 是一种豁免机制,而不是信号。 别名命中或当前工作目录命中都不会计入任何一项证据数量,因此,如果某个范围纯粹凭借 6.0 或 4.0 胜出,那么对下面的门槛来说,它的证据数量就是零。不过,它会跳过这两个门槛:只有在没有其他信息能确定指代对象时,指示性表达才会成为问题,所以一个既说“它”又点名项目的问题不受影响。

out_of_group 是根据整个语料库而非组来判断的。 只有当语料库范围内的胜出者不在该组中,并且它本身也会触发路由时,才会触发此条件。若没有第二项测试,它就会完全吞掉另外两个原因——对于与任何词汇都不匹配的问题,每个范围的得分都接近零,胜出者只取决于谁在 0.15 的近期性平局判定中胜出。

三个证据门槛采用 OR 关系,因为它们分别针对不同的问题形式。 关于罕见符号的简短问题证据量高、数量少;关于熟悉子系统的长问题则相反。要求三者都满足,会导致两种情况都无法作答。

枚举会反转其他所有门槛的逻辑,因此需要单独的选择规则。 上述每个门槛都在问是否有足够的证据支持某一个范围,而询问多个项目共有某个事物的问题会按其设计原理将证据分散到各个项目中——真正共享某个术语的项目越多,它们全部被选中的可能性就越低。实测中,which of my projects use Cloudflare workers or D1? 返回的两个归属者中只有一个:2.098 对 1.0867,而 0.85 的比例要求达到 1.783。

比例是反转逻辑的部分。它衡量与最高分范围之间的距离,而枚举问题的最高分范围只是最常提到该术语的归属者。集合模式会舍弃这一比例,转而依次向每个范围提出相同的可路由性问题,并将门槛降低至 0.8——因为集合中的成员所需的证据量本来就可以低于单一答案。

枚举和指示性表达一样,属于语法现象,因此采用相同方式检测:使用封闭的标记集合,不设阈值。它的优先级还高于指示性表达。"where else does this pattern appear?" 既指向其主题,又要求跨语料库查找;指示性规则之所以存在,是因为指向性问题无法确定单个范围,而枚举问题本来就不是在选择一个范围。

查询中的框架名词会被剥离,这是对指示性表达洞见的进一步延伸——projects 是结构性成分,而不是词汇。这并非表面处理:在开发语料库中,恰好只有两个范围包含 projects,因此它被计作这两个范围的区分性证据,并让它们卷入每个“which of my projects”问题的答案中。剥离该词后,负样本族的弃答率从 66.7% 升至 88.9%。

loci ask "which of my projects use Cloudflare workers or D1?"
# ENUMERATED -> 3M1RY33T.github.io, urthreads
范围与组

组是覆盖在扁平范围集合上的标签。任何地方都没有树结构, 也不会合并任何内容。

      scope                        the groups it carries
      ───────────────────────────  ──────────────────────────────────────
      delroy           monorepo    me    delroy
      delroy/glasses   sub-scope   me    delroy
      delroy/client    sub-scope   me    delroy   client:acme
      acme-api         repo        me             client:acme
      vendorlib        repo                                     vendor:someorg

      me, vendor:someorg   read from git provenance by `scan` or `groups infer`
      delroy               computed by `scan` from the filesystem: a repository
                           that splits gives its own id to every sub-project
                           inside it, and to itself
      client:acme          asserted by hand, with `loci group add`

五个范围,四个组。delroy/glasses 并未存储在 delroy「内部」;它携带 delroy 标签,正如它也携带 me 标签一样。client:acme 横跨一个单仓库子项目和一个无关的仓库,而层级结构根本无法表达这种情况。这两个子范围之所以存在,仅仅是因为该仓库在 .loci.json 中声明了它们,或者扫描时使用了 --split;两者都没有的话,delroy 就是一个范围,包含组也就不存在。父范围会从自身集合中排除其子范围的子树,因此不会重复计数任何文件,双方的词汇表也不会被对方的词元膨胀。

组对问题的作用称为模式。根据组的触发方式,模式会回答两个不同的问题:

                    reached through your cwd        named with --group X
  ────────────────  ─────────────────────────────   ────────────────────────
  explicit          nothing                         confines to X's members
  soft (default)    every outside scope's           confines to X's members
                    evidence base × 0.5
  hard              confines to members, and        confines to X's members,
                    abstains when the best          and abstains when the
                    answer is outside               best answer is outside

右侧一列是违反直觉的部分,因此这里用实测结果,而非断言来说明。在一个包含三个范围的测试样例中,alpha 和 beta 属于组 team,vend 不属于该组;同一个问题,四种策略:

loci route "how does the gizmo parser emitter handle sprocket calibration"

  unconfined                  -> vend, alpha
  --group team   explicit     -> alpha
  --group team   soft         -> alpha
  --group team   hard         -> ABSTAIN (out_of_group); candidates: alpha (sprocket), beta (calibration)

三种模式都排除了不受限制时胜出的范围 vend。使用 --group,表示你断言答案就在该组内;模式只决定答案不在其中时该如何处理。hard 下的候选列表刻意比上方的选定结果更宽:它列出的是弃答时无法在其中做出选择的范围,而非它会从中给出的答案——beta 在候选列表中,因为 calibration 是只有 beta 包含的词;但它不在答案中,因为它仅包含这个词。

如果改为通过当前工作目录触发,soft 会降低优先级,但不会限制范围。同一个问题从 alpha 内部提出,且完全不使用 --group 时,会返回 alpha, vend:vend 的得分为 3.39,alpha 为 6.18——远远超出 0.85 的扩展范围——但它仍会被返回,因为它是唯一包含 gizmo、parser 和 emitter 的范围,而高集中度层级会强制将所有包含共享或稀有词的范围纳入答案。降低优先级不等于排除。


命令

命令用途
loci setup [dirs…]一次性扫描、构建图、建立索引、生成嵌入并校准(-y、--no-embed、--split)
loci update [dirs…]刷新已注册范围的图、索引、向量和校准(--no-scan、--force)
loci scan <dirs>发现 git 仓库并将其注册为范围(--split)
loci add <path>显式注册一个范围(--alias、--glob)
loci scopes列出已注册的范围(--group)
loci uses查看哪些项目引用了其他项目,并附带引用依据(--refresh)
loci groups列出组、解析后的模式及各组包含的范围数量
loci groups infer根据 git 来源为每个范围添加 me 或 vendor:<org> 标签
loci group set <group> --mode设置组的限制程度:explicit、soft、hard
loci group add|rm <scope> <group>编辑一个范围的组成员身份
loci graphs [scope…]通过 graphify 构建缺失的结构图
loci index构建路由索引和事件存储(--force)
loci embed在本地对事件块进行编码(--model)
loci calibrate根据语料库拟合证据阈值(--show)
loci route "…"显示问题的路由结果(--explain、--group)
loci ask "…"先路由,再查询两个存储(--scope、--group、--fast、--rerank)
loci doctor检查每个范围的覆盖缺口及对应的修复方法
loci eval测量语料库上的路由效果(--misses)
loci skill install为 AI 客户端安装 /loci 技能(--dir、--print)
loci mcp通过 stdio 运行 MCP 服务

loci index 会复用文件和 git HEAD 均未变化的范围,因此重新建立索引的成本很低,足以在提交钩子中运行。

保持最新

没有任何内容会自动更新。没有监视器、没有守护进程,也没有安装的 git 钩子:每次刷新都需要运行一条命令。

loci update            # the whole chain, for everything registered

对于已有安装,这是 setup,因此它不会提示任何内容,也不会让你选择任何新功能:嵌入会重新编码,因为你已经有嵌入;阈值会重新拟合,因为它们之前已经拟合过。 它还会重新扫描上次扫描的位置,因此之后新建的仓库会被登记,而不是遗漏——这就是 scopes.json 中 roots 键的用途,loci update ~/work 则会添加另一个位置。

它会无条件重建的一项内容是结构图,因为这是 loci 中唯一一种会悄无声息地过时的内容。loci graphs 会为缺失的图构建图,并跳过已有图的每个范围,因此代码发生移动的项目会继续根据它注册时的符号进行路由。其他所有内容都会显露出来:index 会重新解析指纹发生变化的范围,而 doctor 会指出与存储不再匹配的嵌入。全部重建的成本并不高,因为 graphify 不会重写代码未变化的图——实测连续运行两次后,graph.json 的字节内容完全相同,mtime 也未改变;而 loci 自身的指纹会读取该 mtime,因此刷新图不会强制重新索引未发生变化的范围。

手动执行时也是同样的操作,顺序仍然是依赖链:

loci graphs --all      # `loci graphs` alone would skip every existing graph
loci index             # unchanged scopes are reused, not re-parsed
loci embed             # a full re-encode; skipping it silently disables
                       # semantic search for every scope whose chunk count moved
loci calibrate         # the floor measures shared vocabulary, which just changed

update 有两件事无法替你完成。已从磁盘删除的项目仍会留在注册表中——系统会报告它,而不会将其移除,因为该条目包含你手动设置的组和别名,而且该目录也可能只是暂时卸载了。运行时间较长的 loci mcp 服务会在内存中保留启动时拟合的排序器和向量,因此重建后要重启它。


在 AI 客户端中使用

loci skill install          # -> ~/.claude/skills/loci/SKILL.md

这会安装一个带子命令的技能,而不是在列表中塞入八条命令:

/loci "<question>"          ask memory; routes on your working directory
/loci update                refresh everything registered
/loci doctor                coverage gaps per project, and the fix for each
/loci scopes                what is registered, and the group each is in
/loci route "<question>"    where a question routes, and why it abstained
/loci add <path>            register one project
/loci setup [dirs…]         first run
/loci <anything else>       passed through to the `loci` CLI verbatim

透传规则让它保持简洁:index、embed、graphs、 calibrate、eval 和 groups 都可以直接从客户端使用,无需重复记录,因此技能不会偏离它从未提及的标志。它确实提及的内容都会经过检查——有一项测试会解析 Usage 区块,并在其中任何命令不再存在时失败。

技能随软件包一起发布,而不是放在 dotfiles 仓库中,因为它记录的标志和弃答原因可能会随着 CLI 改变;升级后运行 loci skill install 就是完整的更新流程。对于不读取 ~/.claude/skills 的客户端,--print 会将内容写入 stdout。

它会教会 Agent 两件 CLI 中不明显的事。从项目根目录运行——当前工作目录是主要路由信号,因此从错误目录提问,比不指定目录时的路由效果更差。还有,ABSTAINED 也是一种答案:原因会指出接下来该做什么,其下的候选列表会列出可能负责该问题的项目,以及将它们列入其中的术语;Agent 若改写问题后重试,就是在一个关于词汇而非措辞的判断上浪费轮次。唯一一种改写也无济于事的情况,是弃答结果完全没有候选列表——语料中没有任何内容包含问题里的独特术语。

MCP

loci mcp 使用 stdio,因此由客户端启动——你无需手动运行它。

Claude Code:

claude mcp add loci --scope user -- loci mcp
claude mcp list                       # loci: ... - ✔ Connected

Claude Desktop,在 claude_desktop_config.json 中:

{
  "mcpServers": {
    "loci": { "command": "loci", "args": ["mcp"] }
  }
}

如果 loci 位于由版本管理的环境中,请为 command 使用绝对路径——pyenv shims 和 pipx venv 会根据客户端无法访问的 shell 解析路径。在可用的 shell 中运行 which loci,然后粘贴目标路径。

只有三个工具——ask、scopes、doctor,而不是几十个。Agent 是主要使用者,过多的工具会把编排工作推给模型,并且每跳都要消耗一个轮次。

没有这个服务,服务器就毫无用处:它通过 mcp extra 安装,却没有任何东西能访问它;Agent 的每个问题都会改走 CLI——每个问题都要承担完整的进程和模型启动开销,也永远无法利用服务器启动时预热所提供的能力。

客户端知道 cwd 时,调用 ask 就传入它;也可以传入 group,以与命令行 --group 相同的方式缩小范围。服务器会在启动时加载嵌入模型和最大范围的排序器,因此 Agent 发出的第一个工具调用就能快速响应,无需让用户等待约 4.4s 的启动时间。


在自己的语料库上测量

以下所有准确率数据都基于某一个人的代码库进行拟合和测量——拟合常量时有十个,现在有十四个。否则,你只能选择相信这个说法,而这两个数字之间的变化让行为系列损失了 57 个百分点。

loci eval            # a few seconds, no labelling, no setup
loci eval --misses   # and what it got wrong

它只会提出通过构造即可确定正确答案的问题:

系列如何确定标准答案
指示性 + cwd在某个范围内提出问题,因此答案就是该范围
指示性,无 cwd没有信息指出是哪个项目——不作答是唯一正确的答案
无法回答软件语料库都无法回答的无意义问题
特征词根据每个范围中最具辨识度的词汇构造
10 scopes | random guessing would score 10.0%

family                  n  correct  scopes  what it measures
deictic + cwd          80  100.0%       -   should route to the scope you are in
deictic, no cwd         8  100.0%       -   should abstain; nothing says which project
unanswerable            6  100.0%       -   should abstain; no corpus can answer these
signature              20  100.0%     1.0   are your scopes distinguishable from each other?

特征词系列是一个上限——它会针对每个范围中最罕见的词提问,因此,用不同范围共有的词汇表述的真实问题表现会更差。这项得分低说明了你的项目情况,不一定是需要调优消除的错误。

校准

路由会检查三个互相独立的信号,任意一个通过即可:一个辨识度极高的词元、足够的词元证据总量,或足够多的匹配词元。它们会在不同类型的问题上失效——关于罕见符号的简短问题具有较高证据值但匹配数量少;关于熟悉子系统的长问题则相反——因此,要求三者全部通过会导致这两类问题都不作答。

证据下限取决于你的语料库,因为它取决于项目之间共享多少词汇。loci calibrate 会根据同一批自动标注的问题拟合该下限,并报告各区间是否能够区分:

evidence floor      7.17
fitted from         149 routable + 14 unroutable questions, none hand-labelled
classifies          84.0% of its own samples correctly

! The bands OVERLAP -- some questions that should route score no higher than
  questions that should not, so no threshold separates them cleanly.

重叠警告反映的是你的项目情况,而非缺陷:项目共享的词汇足够多,单靠证据并不总能区分它们;这时应更多依靠 cwd 或 --scope。


它读取什么,又写入什么

所有内容都留在你的机器上。查询路径中不使用大模型;路由对每个 token 都是一次 dict 查找。启用嵌入时,也会在本地运行。

每个作用域都会收集:README*、docs/**/*.md、源文件(用于提取文档字符串和注释块)以及 git log。使用 loci add --glob 添加其他内容,包括仓库外的绝对路径。

loci 在仓库内读取的唯一配置是 .loci.json,而且只有在你创建该文件时才会读取。它可以指定任意深度的子项目;与 --split 不同,每次扫描都会遵循它:

{"scopes": [{"path": "client"}, {"path": "services/api"}]}

路径相对于仓库根目录,且不得越出仓库。格式错误的文件会被忽略——扫描因一个多余的逗号而中止,远不如漏掉一条声明。此文件不会由任何程序写入;由你来写。

loci 在仓库内写入的唯一内容是 graphify-out/graph.json。有两个命令会写入该文件:loci graphs,除此之外不做任何事;以及 loci setup,它会在 5 个步骤中的第 2 步询问是否创建,默认选项为“是”——即使没有终端,每个提示也都会采用默认选项。loci setup --no-graphs 会跳过该文件,其他命令则只会读取它。

因此,在仓库内,恰好只有两个文件会产生影响,而且它们的指向相反:

<repo>/graphify-out/graph.json    loci writes it, on request. You never do.
<repo>/.loci.json                 you write it. loci never does.

其他内容都存放在 ~/.loci(或 $LOCI_HOME)中。所有项目共用一个目录,而不是每个项目各用一个目录:路由是一项跨作用域的决策,因此,要回答“这与哪个项目有关”,就需要在同一次查找中使用所有作用域的词汇;若为每个项目分别建立 N 个索引,就会把一个轻量查询变成昂贵操作。

~/.loci/                    what it holds               written by

├── scopes.json             the scope registry, and     scan, add, group,
│                           the roots it was scanned    update
│                           from, for `loci update`
├── groups.json             group policy: default mode  group set
│                           and any mode set per group
├── scope_index.json        routing index, the token    index
│                           -> {scope: node_df} map
├── episodes.json           the episode store: chunks   index
│                           verbatim, already redacted
├── rankers/<scope>.lex     fitted BM25 + char-gram     index
├── embeddings.npz          chunk and symbol vectors    embed
├── .symbols-<scope>.json   symbol labels, in the       embed
│                           order embeddings.npz has
├── calibration.json        the fitted evidence floor   calibrate
│                           + a semantic floor / scope
└── .index.lock             advisory build locks, so    index, embed
    .embed.lock             two builds cannot inter-
                            leave their writes

scopes.json 由程序管理,每次扫描都会整体重写;groups.json 则由你管理,扫描时必须保留。因此,成员关系存储在注册表中,模式存储在策略文件中,也因此需要两个文件。

敏感信息脱敏

所有收集的内容在写入前都会经过 loci.redact,所以意外提交的凭据绝不会进入事件存储、向量或排序器。

专门用于存放秘密信息的文件绝不会被打开——.env*、.dev.vars、*.pem、*.key、id_rsa、secrets.*、credentials.json。随后,系统会扫描每个文本块,查找 AWS 密钥、GitHub/Slack/Google/Stripe/OpenAI/Anthropic 令牌、私钥块、JWT、user:password@host 连接字符串、bearer 令牌,以及赋给名称看起来像秘密信息的变量的长不透明值。

文本块都通过单一构造器生成,因此新的收集路径不会遗漏脱敏处理。秘密赋值会保留键名,只去掉值,这样文本块仍然易于理解,也仍然可以用于路由:

client_secret = "aVeryLong..."   ->   client_secret = "[REDACTED:secret-assignment]"

loci index 会按作用域和类型报告删除的内容——静默脱敏与未脱敏无法区分。设计上有意倾向于过度脱敏:误报会稍微降低一个文本块的检索质量,而漏报则会把有效凭据复制到明文文件和向量索引中。


设计规则及其背后的测量结果

这些都是合理的实现容易出错的地方。

1. 切勿将多个作用域合并到一个索引中。 请看顶部的表格:针对合并图谱,相关度分别为 2% 和 18%。

2. 两层都应支持弃答。 在包含故意胡言乱语的探测中,测得 10/10。每个 RAG 工具都会自信地回答“无载重燕子的飞行速度是多少”;而这个工具会坦言自己不知道。

3. 边界和比率测试无法衡量置信度。 两种方法都试过了,在真实数据上都会得出相反结论。“如何修复这个 bug?” 得出的路由边界为 0.94,而一个正确路由的问题得出的值为 0.18。与小型作用域中的真实问题相比,胡言乱语在大型作用域中的得分为 0.608,最高分与第 90 百分位分数之比更高。真正的区别在于,问题中的词是否存在于语料库中。

4. 匹配词元数量并非证据。 与自动标注的问题相比,数量完全无法区分可路由与不可路由的问题,甚至得出相反结论。词元证据总和则可以区分。数量仅作为三个 OR 门之一保留。

5. 指示语属于语法现象,因此应通过语法来检测。 指向自身主题的问题(“这个项目”“这个应用”“它”)无法通过词汇进行路由,因为能识别主题的词恰恰是它没有说出的词。任何词汇统计方法都无法补救这一点:start(2.51)和 services(2.56)的单词元证据,与 session(2.53)这样的真实证据无法区分。使用一组封闭的标记后,弃答率从 37.5% 提高到 100%。

6. 按作用域大小进行两次归一化。 当一个作用域比其他作用域大 67 倍时,普通英语单词会只出现在一个作用域中,从而在该作用域中获得最高的区分度。

7. 文档字符串是极佳的检索材料,却不是好的路由材料。 它们包含大量泛化英语词汇,如“返回”“该值”“默认”“配置”;将其纳入路由后,基于符号索引和仅基于文本的语料库的准确率都降低了一半。

8. 覆盖率才是关键限制因素,而不是排序。 每次实验都发现了这一点。在三个未建立索引的仓库上运行免费的纯 AST 索引后,路由率从 69.6% 提高到 82.6%,同时还增加了两个竞争作用域。因此,doctor 被设为一级命令:给出原因的空答案,好过来自错误项目的自信回答。


排序

情节搜索会融合三种信号,并根据实际产生信号的排序器重新归一化:

排序器擅长识别
BM25精确术语、罕见标识符
字符 3–5 元语法词形变化和大小写差异——samesite 与 SameSite=None
嵌入(bge-small,本地,可选)没有共同词汇时的语义

词汇排序器组合可以找回结构图谱完全无法识别的问题。嵌入排序器则能覆盖两种词汇排序器都无法处理的情况:问题与其答案没有共同词汇。

加入嵌入后,弃答功能失效了——每个片段的得分都大于零,因此绝对阈值不再生效。解决方法是采用双层门槛:命中项必须有词汇依据,或具有足够的语义置信度,因为单独使用词汇门槛会恰好拒绝加入嵌入原本要处理的内容。

可通过 --rerank 使用交叉编码器重排,但默认关闭:在六个标签本身就有争议的案例中,precision@1 从 2/6 提高到 3/6,单次查询约耗时 96ms,同时还出现了两处小幅退步。

性能

loci index          15s full · 3.9s when nothing changed
loci ask            1.0s one-shot with --fast · ~5s with semantic ranking
MCP server          6s at boot, then 0.2-0.3s per tool call
routing             sub-millisecond, and flat from 25 to 100 scopes

三个因素让这一切成为可能,每个因素首先都针对一个经过测量的瓶颈:

遍历会剪枝,而不是筛选。 Path.glob("**/*.py") 会完整遍历 node_modules、.venv 和 Pods,枚举一个代码仓库就要 32.9s。 剪枝不仅提升了速度,也改善了索引:某个项目在剪枝前有 66% 的源码来自供应商引入的第三方代码。

词法排序器在索引时拟合。 对大型范围进行字符 3–5 元语法 TF-IDF 计算需要约 1.5s,而进程内缓存无法跨 CLI 或 MCP 调用保留。

嵌入模型在 MCP 启动时加载。 约 2.3s 的导入和模型构建属于进程启动开销,而非实际工作。对于一次性 CLI 问题,--fast 会跳过这一步。

规模
范围数词汇重叠路由耗时Top-1
25–100低0.1–0.5ms100%
25–100中等0.3–0.6ms90%
25高0.1ms能给出答案,但通常是错的(36%)
50–100高0.5ms90% 的情况下会弃答

范围数量不是限制因素,词汇重叠才是。随着范围数量增加,失败会变得更安全——共享某个术语的范围越多,任何单个范围的证据就越少,因此门控会触发。由高度相似项目组成的小型代码库才是危险配置。


持久性

写入会先通过临时文件,再调用 os.replace,因此读取者不会看到只写了一半的索引。Path.write_text 会先截断文件再填入内容:在一个 8MB 的存储上测得,并发访问几秒内出现了 11 次撕裂读取,修改后则降为 0。MCP 服务在 loci index 运行时读取,恰好就会遇到这种情况。

loci index 和 loci embed 会持有咨询锁,因此两个构建任务不会交错运行并生成彼此不一致的索引和存储。已退出进程遗留的锁会被检测并解除。

存储会先于索引写入,因为读取者以索引为门控:如果两者之间发生崩溃,留下的会是一个指向更大存储的过期索引,而不会是一个声称包含实际不存在的代码块的索引。


扩展

backends/base.py 中的两项契约:

class StructureBackend(Protocol):
    def sources(self, scope) -> list[dict]: ...
    def vocabulary(self, scope) -> Counter: ...      # feeds the router
    def query(self, scope, query, *, budget, dfs) -> list[StructureHit]: ...

class EpisodeBackend(Protocol):
    def collect(self, scope) -> list[Chunk]: ...      # verbatim, redacted
    def search(self, question, chunks, scope_id, *, k, rerank) -> list[EpisodeHit]: ...

backends/ 以上的所有代码都不知道当前使用的是哪种实现。将 graphify 换成 tree-sitter、SCIP 或 LSP 索引,只需编写一个适配器,无需修改路由器。search 必须能够返回空列表——一个总能找到内容的存储在必要时无法被信任。

为什么它不是 graphify 的分支

loci 通过一个适配器依赖 graphify,该适配器读取其 graph.json 数据契约,并通过 shell 调用 graphify query --graph。分叉就意味着继承一个 767KB 的提取器和约 18 种 tree-sitter 语法,只为修改本来无需改动的代码。促使 loci 诞生的范围划分缺口——graphify 会给合并图中的节点标记 repo 属性,却没有提供按该属性筛选的方式——通过在 graphify 之上为每个范围保留一张图就能解决。两个项目都采用 MIT 许可证,因此仍可选择分叉;只是没有必要。


状态

Alpha 阶段,相关数字仅供参考。

在涵盖 14 个范围的 173 个条目集上进行测量。其中 112 个条目由固定分类体系应用于每个范围生成,因此这部分在构建时就不存在偏差。其余 61 个条目由构建路由器的同一人手动编写;每个条目都带有一个 contamination 字段,记录作者当时已知的信息,并且始终单独报告未受污染的结果。

评估语料库不在此仓库中。 测试工具会读取 14 个真实项目,其中 6 个是私有项目或从未推送到任何地方,而且记录中会以 file:line 的形式注明这些项目。因此,evals/ 以及这些来源中反复提到的 evals/RESULTS.md 都保存在本地,不会发布在此处或 sdist 中。实际随包提供的是 loci eval,它会根据你注册的语料库生成问题;这个数字才值得用来评估你自己的配置,也是本 README 建议你运行的命令。

已知限制:

  • 语料库从 10 个范围扩展到 14 个后,行为类的准确率从 85.7% 降至 28.6%,原因并非阈值问题——在新语料库上将 SIZE_PRIOR 从 0.0 扫描到 0.5,效果始终不如随包提供的 0.15。现在有两个范围分别包含 5,995 和 4,680 个不同词元,而 urthreads 只有 427 个;某个范围的词汇量一旦接近整个语料库的词汇量,就会匹配普通英语中的每个问题。针对该问题测试了五种替代评分方法,没有一种优于简单求和;唯一有所改进的方法以未启用的 CORROBORATION_WEIGHT 发布,因为单个语料库中的单个条目不足以构成证据。这是路由器目前最大的问题。
  • 合成测试环境无法复现这一故障:CorpusSpec.size_skew 改变的是文件数量,而非词汇广度,因此无论使用哪种本可解决问题的常量值,全部 13 种形态的读数都是 100%。要解决这一问题,必须修改测试环境,而这会阻碍路由器的修改。
  • 本次发布期间发现有两项评估标准过时,其中一项将正确的三范围答案误判为精确率失败。语料库发生变化会导致正确答案被记录为未命中,而测试工具对此毫无察觉。
  • 结构存储在 7 次探测中,有 4 次找到了回答问题的符号。
  • 纯文本内容庞大的范围容易吸引过多问题;某个范围即使有数千个片段且没有代码图谱,也会吸收本应属于其他范围的问题。
  • CI 现在会在 Linux、Windows 和 macOS 上使用 Python 3.10–3.13 运行测试套件。搭建 CI 时发现了一个阅读代码时未察觉的第三个平台问题:如果目标文件正被另一个句柄打开,Windows 会拒绝对其执行 os.replace,导致所有持久化写入都失败,而另外两个平台的测试全都通过。阅读代码确实能发现问题——但没发现这个。目前还没有用户在 Windows 上运行过 loci。
  • loci eval 和 loci calibrate 使用相同的问题类别,因此校准后立即获得完美的 eval 分数,部分原因是循环验证。

最有帮助的做法,是在不属于我的语料库上运行 loci eval,并告诉我们结果如何。


许可证

MIT。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。