cangjie-knowledge-mcp
by sskacc
README.md
# cangjie-knowledge-mcp
Cangjie 知识库 + MCP 服务器,为 Java → Cangjie 片段翻译提供 API 检索能力。
在翻译一个 Java 片段之前,先通过 MCP 工具做检索,找到片段中用到的类/方法/类型在
Cangjie 标准库中的**来源(哪个库、哪个模块)**、**完整签名**、**官方示例代码**;翻译失败
进入错误修复循环时,也可以调用本工具(`error_fix_hint`)根据编译错误定位相关 API 和示例。
## 它能回答什么问题
| 场景 | 用法 |
|---|---|
| `HashMap.put(key, value)` 在 Cangjie 里怎么调? | `search_api("map put key value")` → 找到 `HashMap` 的 `add(K,V)`/`replace(K,V)`(Cangjie 没有 `put`) |
| `ArrayList` 有哪些方法? | `get_class_members("ArrayList")` → 成员及签名 |
| 读文件怎么写? | `find_examples("read file bytes")` → 官方 sample 代码 |
| `java.util.List` 对应 Cangjie 什么? | `java_to_cangjie("java.util.List")` → j2cjlib 映射 |
| `cannot find symbol println` 怎么修? | `error_fix_hint("...")` → 相关 API 文档 + 示例 |
| 一段 Java 代码怎么翻译? | `resolve_java_code("<java代码>")` → per-call 建议(见下文) |
## 核心特性:per-call 渐进式披露检索
**对代码块中的每个 API 调用独立检索,每个调用产出一个 suggest**——包括**构造调用**
(`new Xxx(...)`,每个构造也单独建议,含 `new HashMap<>()` 菱形泛型)。细粒度(api)
没有一一对应时,自动上升到语句级(statement),再不行才为整个代码块生成块级
suggest。在此之上叠加**两阶段检索**(类型锁定 → 方法匹配)+ **语义 rerank**,
让结果从"候选列表"变成"一组可直接使用的建议"。
### 三层粒度
```
Level 1 api 最细粒度: 单个 API 调用 "map.put(k, v)"
→ 锁 receiver 类型(HashMap) → 该类中与调用意图最匹配的成员
→ 产出该调用的 suggest
Level 2 statement 中间粒度: 整行语句 "map.put(k, v);"
→ NL 检索,对应 Cangjie 的一个或几个 API
→ 用于 receiver 类型不可锁定时的降级
Level 3 function 最粗粒度: 整个代码块 "public void copyFile(...) {...}"
→ NL "copy a file",检索整个功能模块
→ 只生成一个块级 suggest(兜底)
```
### per-call 工作流
`resolve_java_code` 的完整流程:
```
输入: BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String line = reader.readLine();
│
├─ extract_types() 拆出每个调用(含构造调用 `new Xxx(...)` 与 `new HashMap<>()`)
│ calls = [reader.readLine, new BufferedReader, new InputStreamReader, ...]
│
├─ 对每个 call 独立解析:
│ ┌─ L1 api: 锁 receiver 类型(查 x2cangjie 映射表 / BM25 类名兜底)
│ │ → 该类成员按该调用的 NL 意图重排(含 rerank)→ 返回 top-k 成员
│ │ (map.put → HashMap 的 add/contains/...;reader.readLine → StringReader 的 readln/...
│ │ new BufferedReader → StringReader 的 init(T)/lines/... ← 构造调用单独建议)
│ ├─ L2 statement: receiver 类型无法锁定(如 System.out.println)时,
│ │ 用整行语句 NL 检索 → 返回候选 API 列表
│ └─ L3 function: 整个块仍无法解析时,生成一个块级 suggest(兜底)
│
└─ 输出 suggestions[]: 每个调用一个建议
```
**关键点**:
1. **每个 API 调用都有自己的 suggest**——`map.put` 和 `reader.readLine` 是不同类型
的不同意图,各自锁各自的类、返回各自最匹配的成员。**构造调用也单独建议**。
2. **api 级只返回 top-k 成员**(默认 5)而非整个类;粗粒度(statement/function)才返回完整列表。
3. **升降级规则**:receiver 类型可锁定(含构造调用的类名)→ api;不可锁定 → statement;
整块都无法解析 → function。
4. **类型锁定**:查表(精确)+ BM25 类名兜底(相似度),泛型会被剥掉(`HashMap<String,Integer>` → `HashMap`)。
### 语义 rerank(可选增强)
在 BM25 召回之后,用一个**可选的 LLM rerank 层**对 top-k 候选做语义重排,纠正
BM25 在"词形不同但语义相同"场景下的盲区(如 Java 的 `readLine` → Cangjie 的
`readln`,`readln` 词法上拆不出 "read"+"line",BM25 会打 0 分,但 rerank 能理解
"读下一行"就是 `readln`)。思路来自 LongCodeZip 论文(条件困惑度排序比词法相似度
高 7.89%),实现上用 LLM 直接输出候选排序(避免依赖 token 级 log-prob)。
- **recall-rerank 两段式**:BM25 先召回 top_k×3(粗筛),LLM 只精排几十个候选。
- **零依赖回退**:无 `api_key` 或 `rerank=false` 时,顺序与纯 BM25 完全一致。
- **容错**:LLM 超时/报错/输出不可解析,一律回退 BM25 顺序,检索永不因 LLM 失败而退化。
## 架构
```
cangjie-knowledge-mcp/
├── config.yaml # 语料路径、索引参数、LLM 配置(api_key 走环境变量)
├── Dockerfile # MCP server 容器化镜像
├── opencode.json # opencode MCP 注册配置
├── src/cjkb/
│ ├── models.py # ApiRecord / ExampleRecord / JavaMapping 数据模型
│ ├── config.py # 配置加载(支持环境变量覆盖)
│ ├── java_types.py # Java 类型提取器(声明/泛型/调用接收者/构造调用/强转)
│ ├── nl_generator.py # Java 代码 → 中英双语 NL 描述(LLM 或启发式)
│ ├── layered_search.py # per-call 解析:类型锁定 + 分层 NL + 升降级 → suggestions[]
│ ├── reranker.py # (可选)LLM 语义 rerank 层
│ ├── collector/
│ │ ├── corpus_parser.py # 解析 CangjieCorpus 官方文档 → API/示例记录
│ │ ├── j2cj_parser.py # 解析 j2cjlib shim + 术语表 → Java→Cangjie 映射
│ │ └── example_writer.py # (可选)LLM 为缺少示例的 API 生成示例
│ ├── index/
│ │ ├── bm25.py # 纯标准库 BM25(字段加权, 驼峰/下划线/中文 unigram 分词)
│ │ └── searcher.py # 检索 API(相似度 + 精确名 + Java 术语扩展)
│ └── mcp_server.py # MCP stdio 服务器(零第三方依赖)
├── scripts/
│ ├── build_kb.py # 收集 + 建索引 → data/
│ ├── import_type_mappings.py # 导入 x2cangjie 类型翻译产物
│ ├── install_kb.py # 一键就绪:校验数据 + 自动重建索引
│ ├── generate_examples.py # (可选)LLM 补写缺失示例
│ └── query_demo.py # 命令行检索演示
├── tests/ # 单元测试 + 综合端到端测试
└── data/ # 知识库(JSONL 入库, pkl 派生不入库)
```
### 数据流
```
CangjieCorpus(官方文档) x2cangjie 类型翻译产物 j2cjlib shim + 术语表
│ │ │
v v v
corpus_parser.py import_type_mappings.py j2cj_parser.py
└────────────────────────┼──────────────────────┘
v
KnowledgeBase(JSONL) ──> BM25 索引 ──> MCP server
│ (stdio, 9 个工具)
v
Java 代码 → extract_types → resolve_java_code(per-call)
```
### 检索原理
1. **分词**:驼峰拆分(`getOrThrow` → `get or throw`)、下划线拆分(`read_file_bytes` →
`read file bytes`)、**中文 unigram 单字切分**(中文无空格词边界,单字切分让 BM25
能对中文查询/描述打分,无需引入分词器)。
2. **BM25 字段加权**:`name × 4` > `signature × 3` > `module × 2` > `tags × 1.5` >
`description × 1`,让"按名检索"比"按描述检索"更准。
3. **Java 术语扩展**:查询 token 先查 Java→Cangjie 映射表,把 Java 词汇展开成
Cangjie 同义词再检索(解决 `Thread` vs `线程` 的匹配问题)。
4. **精确名索引**:`get_api_details` / `get_class_members` 走精确名索引,不依赖相似度。
### MCP 工具(9 个)
| 工具 | 说明 | 典型调用 |
|---|---|---|
| `search_api` | API 相似度检索,返回签名/模块/来源/描述 | `search_api("HashMap put key value")` |
| `get_api_details` | 按名精确查函数/类/接口 | `get_api_details("add", module="std.collection")` |
| `get_class_members` | 类的全部成员(init/prop/func) | `get_class_members("ArrayList")` |
| `find_examples` | 检索示例代码 | `find_examples("read file lines")` |
| `java_to_cangjie` | Java 符号 → Cangjie 等价物 | `java_to_cangjie("java.util.List")` |
| `error_fix_hint` | 编译错误 → 相关 API + 示例 | `error_fix_hint("cannot find symbol println")` |
| `list_modules` | 列出知识库中所有模块 | `list_modules()` |
| `resolve_java_code` | **per-call 渐进式披露**:拆成每个 API 调用,逐个锁类型+分层检索,返回 `suggestions[]` | `resolve_java_code("map.put(key, value);")` |
| `describe_java_code` | 只生成 Java 代码的中英双语 NL 描述(不检索) | `describe_java_code("String line = reader.readLine();")` |
## 快速开始
**知识库数据(JSONL)已随仓库托管在 GitHub**——clone 即用,不需要在本机重新构建。
BM25 索引(.pkl)是派生产物,首次运行时自动重建。
```bash
# 1. clone + 安装依赖(仅 PyYAML;索引核心零依赖)
git clone https://github.com/sskacc/cangjie-knowledge-mcp.git
pip install -r requirements.txt
# 2. 一键就绪:校验数据 + 自动重建索引(首次运行会自动完成,可跳过)
python scripts/install_kb.py
# 3. 命令行试一下
python scripts/query_demo.py "HashMap put key value"
python scripts/query_demo.py --class-members ArrayList
python scripts/query_demo.py --java "java.util.List"
# 4. 启动 MCP 服务器(stdio)
python -m cjkb.mcp_server --data-dir data
```
**首次启动 MCP 服务器时**,如果 `data/` 里只有 JSONL 没有 .pkl(刚 clone 的状态),
`Searcher.load` 会自动重建 BM25 索引(约 1 秒),无需手动干预。
### Docker 方式(无需本机 Python)
宿主机没有 Python 时,用 Docker 镜像运行:
```bash
# 构建镜像(打包源码 + 知识库 + PyYAML)
docker build -t cangjie-knowledge-mcp .
# 直接运行 MCP server(stdio)
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test"}}}' \
| docker run -i --rm cangjie-knowledge-mcp
```
### 在 opencode 中注册 MCP
项目根目录已附带 `opencode.json`(opencode 会自动加载项目级配置),它通过 Docker
镜像注册本 MCP server:
```jsonc
// opencode.json(已随仓库提供)
{
"mcp": {
"cangjie-knowledge": {
"type": "local",
"command": ["docker", "run", "-i", "--rm", "cangjie-knowledge-mcp"],
"enabled": true
}
}
}
```
若本机有 Python(无需 Docker),可改为:
```jsonc
{
"mcp": {
"cangjie-knowledge": {
"type": "local",
"command": ["python", "-m", "cjkb.mcp_server"],
"enabled": true
}
}
}
```
注册后,agent 在片段翻译和错误修复时可以直接调用上述 9 个工具。
### 手动维护知识库(推荐工作流)
`data/` 的 JSONL 托管进 git 的原因:**JSONL 是人类可读、可 diff、可 review 的
源数据**,pkl 是机器生成的派生品。维护流程:
```bash
# 1. 编辑 data/*.jsonl(增删 API 记录 / 示例 / Java 映射)
# 直接编辑 jsonl 即可,不必重跑 build_kb.py(重跑会覆盖手动改动)
# 2. 重建索引(改了 jsonl 后必须做,否则检索用旧索引)
python scripts/install_kb.py # 或直接启动 MCP,load 时检测到 jsonl 更新会自动重建
# 3. 提交推送
git add data/*.jsonl
git commit -m "kb: add HashMap add/replace examples"
git push
```
> 规则:**JSONL 是数据,进 git;pkl 是派生品,不进 git**。新机器 clone 后
> install_kb.py / MCP 首启都会自动重建 pkl,无需手动处理。
### 从零重建(只在语料变化时用)
当 Cangjie 官方语料更新,或需要重新收集时。语料来自 **x2cangjie 项目**
(一个 Java→Cangjie 翻译流水线,本知识库是它的配套检索工具),路径按实际
clone 位置填写:
```bash
python scripts/build_kb.py \
--corpus <x2cangjie路径>/misc/CangjieCorpus \
--j2cjlib <x2cangjie路径>/misc/j2cjlib \
--terms <x2cangjie路径>/configs/java_cangjie_terms.yaml
# 重建后导入 x2cangjie 类型翻译产物(类型锁定靠它)
python scripts/import_type_mappings.py \
--type-resolution <x2cangjie路径>/data/java/type_resolution
```
> ⚠️ `build_kb.py` 会**覆盖** data/*.jsonl。若 jsonl 已有手动改动,
> 重跑前先 `git commit`(可回滚),或先备份手动改动。
## 在片段翻译流程中使用
以 x2cangjie 的翻译流程为例,推荐的调用时机:
1. **翻译前**:把待翻译片段丢给 `resolve_java_code` → 得到每个 API 调用的建议,
取 API 签名和示例注入 prompt;细节确认用 `get_class_members` /
`java_to_cangjie` / `find_examples`。
2. **错误修复循环**(`cjpm build` 失败后):把编译错误传给 `error_fix_hint` →
返回相关 API 签名和示例,拼进错误反馈 prompt。
3. **类型解析 RAG 兜底**:把 `search_api` 结果作为额外证据注入类型映射 prompt。
> 说明:本项目是**独立于 x2cangjie** 的新项目,不修改 x2cangjie 的代码。接入方式:
> (a) 在 agent(MCP 客户端)侧注册上面的 server,让 agent 直接调用;
> (b) 在 x2cangjie 的 Python 代码中 import `cjkb` 直接调用 `Searcher`(程序化 API)。
### 程序化调用(不经过 MCP)
```python
import sys
sys.path.insert(0, "src")
from cjkb.config import load_config
from cjkb.index.searcher import Searcher
cfg = load_config("config.yaml")
s = Searcher.load(cfg["output"]["data_dir"], cfg)
for r in s.search_api("HashMap put key value"):
print(r.name, r.module, r.signature)
for m in s.java_to_cangjie("java.util.List"):
print(m.java_symbol, "->", m.cangjie_symbol)
```
## LLM 配置(可选,用于四处)
配置 `config.yaml` 的 `llm` 段或环境变量 `OPENAI_API_KEY` / `OPENAI_BASE_URL` /
`OPENAI_MODEL`。LLM 用于四处:
1. **NL 描述生成**(`resolve_java_code` / `describe_java_code`):LLM 把 Java 代码转成
中英双语 NL 描述。**未配置时自动退回启发式**(驼峰拆分 + 术语映射),检索不受影响。
2. **语义 rerank**(`search_api` / `resolve_java_code` 等检索出口):LLM 对 BM25 top-k
重排。**未配置或 `rerank=false` 时退回纯 BM25 顺序**。
3. **补写缺失示例**:为没有官方示例的 API 生成示例。
4. **编译错误修复建议**(`error_fix_hint`):LLM 直接根据编译错误生成中文修复建议
(解释错误含义 + 修复方法)。**未配置时退回关键词检索**(见下文"变更记录")。
```bash
export OPENAI_API_KEY="..."
export OPENAI_BASE_URL="https://api.deepseek.com" # 可选
export OPENAI_MODEL="deepseek-v4-flash" # 可选
```
## 可选:LLM 生成缺失示例
知识库中**没有官方示例**的 API,可以用 LLM 自动补写(每条标记 `generated=true`,
与官方示例区分)。用独立脚本 `scripts/generate_examples.py`:
```bash
export OPENAI_API_KEY="..."
python scripts/generate_examples.py --dry-run # 看有多少 API 缺示例
python scripts/generate_examples.py --limit 50 # 生成前 50 条(断点续跑)
python scripts/generate_examples.py --limit 0 # 生成全部缺失的
```
特点:
- **断点续跑**:自动跳过已生成的 title,重跑不重复
- **失败容忍**:LLM 空响应自动重试 2 次
- **自动重建索引**:生成后立即更新 BM25 索引
## 测试
```bash
# 单元测试 + 综合端到端测试(需 Docker,或本机 Python + PyYAML)
docker run --rm -v "$(pwd):/app" -w /app cjkb-test:latest python -m pytest -q
# 或本机:
pip install pytest
python -m pytest -q
```
测试覆盖:tokenize(含中文 unigram)、BM25、parser、searcher、MCP 协议、NL 生成、
类型提取(含构造调用/菱形泛型)、per-call 分层检索、降级路径、rerank 回退与解析、
9 个工具端到端、错误处理。
## 变更记录
### 2026-09-01:修复 Java 静态调用解析 + error_fix_hint 检索失配
本次修复两个影响检索质量的问题(均在 `src/cjkb/` 内):
**1. `java_types.py` — 静态调用 receiver 丢首字符**
`_CALL_RE` 正则原要求 receiver 以小写字母开头(`[a-z_]`),只能匹配变量调用
(`map.put(...)`),无法匹配静态调用(`Character.valueOf(...)`)。静态调用会被从大写类名
的第二个字符开始错误匹配,导致 `Character` 被解析成 `haracter`(丢首字母),进而检索到
无关 API。
修复:正则加 `\b` 词边界并允许大写开头;`extract_types` 的 enrich 逻辑对"大写开头的
receiver 且无变量声明"视为静态调用,把 receiver 本身当作类型(`declared_type`)。
**2. `mcp_server.py` — `error_fix_hint` 检索失配**
`error_fix_hint` 原来把编译错误文本(`_clean_error` 提关键词)直接拿去 BM25 搜 API 文档,
但编译错误术语(`mismatched types`、`expected expression`、`undeclared identifier`)与
Cangjie API 文档词汇是两套不相交的词表,导致返回的都是 `hashCode`、`std.fs.init` 等
无关 API。
修复:新增 `_llm_error_hint`(LLM 配置后生效),让 LLM 直接把编译错误解释为**中文修复
建议**(错误含义 + 修复方法,如 `mismatched types` → "可用 `as` 关键字转换");同时新增
`_llm_error_query` 把错误转成检索查询词作为检索的兜底。`error_fix_hint` 返回体新增
`hint`(LLM 修复建议)与 `query`(检索查询词)字段。**未配置 LLM 时**退回原有的关键词
检索,行为不变。
> 注意:`config.yaml` 的 `llm.api_key` 属敏感信息,不要提交实际密钥;用环境变量
> `OPENAI_API_KEY` 注入即可(见上文"LLM 配置")。
### 2026-09-01:`resolve_java_code` 对无映射类型不返回误导建议
`resolve_java_code` 对"没有 Cangjie 对应"的 Java 类型(包装类 `Character`、`Number`、
`StringBuilder`、`StringBuffer` 等)原来仍会走类型锁定 + NL 检索,结果锁到无关的 KB 类
(如 `Character` → `std.core.Object`),返回误导性建议,反而干扰翻译。
修复(`layered_search.py`):
- `_GENERIC_TYPES` 扩展,加入 `Character`、`Number`、`StringBuilder`、`StringBuffer`
等无可靠 Cangjie 对应的 Java 类型。
- `resolve_java_code` 先把这些无映射类型的调用**直接剔除**(`resolvable_calls`),不做
L1/L2 检索;仅当存在可解析调用但都未产出建议时才触发 L3 块级兜底。当所有调用都
无映射时,返回空的 `suggestions`(不再硬塞一个块级兜底建议)。
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues