cangjie-knowledge-mcp
cangjie-knowledge-mcp
Cangjie 知识库 + MCP 服务器,为 Java → Cangjie 片段翻译提供 API 检索能力。
在翻译一个 Java 片段之前,先通过 MCP 工具做检索,找到片段中用到的类/方法/类型在
Cangjie 标准库中的来源(哪个库、哪个模块)、完整签名、官方示例代码;翻译失败
进入错误修复循环时,也可以调用本工具(error_fix_hint)根据编译错误定位相关 API 和示例。
它能回答什么问题
场景 | 用法 |
|
|
|
|
读文件怎么写? |
|
|
|
|
|
一段 Java 代码怎么翻译? |
|
核心特性: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[]: 每个调用一个建议关键点:
每个 API 调用都有自己的 suggest——
map.put和reader.readLine是不同类型 的不同意图,各自锁各自的类、返回各自最匹配的成员。构造调用也单独建议。api 级只返回 top-k 成员(默认 5)而非整个类;粗粒度(statement/function)才返回完整列表。
升降级规则:receiver 类型可锁定(含构造调用的类名)→ api;不可锁定 → statement; 整块都无法解析 → function。
类型锁定:查表(精确)+ 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)检索原理
分词:驼峰拆分(
getOrThrow→get or throw)、下划线拆分(read_file_bytes→read file bytes)、中文 unigram 单字切分(中文无空格词边界,单字切分让 BM25 能对中文查询/描述打分,无需引入分词器)。BM25 字段加权:
name × 4>signature × 3>module × 2>tags × 1.5>description × 1,让"按名检索"比"按描述检索"更准。Java 术语扩展:查询 token 先查 Java→Cangjie 映射表,把 Java 词汇展开成 Cangjie 同义词再检索(解决
Threadvs线程的匹配问题)。精确名索引:
get_api_details/get_class_members走精确名索引,不依赖相似度。
MCP 工具(9 个)
工具 | 说明 | 典型调用 |
| API 相似度检索,返回签名/模块/来源/描述 |
|
| 按名精确查函数/类/接口 |
|
| 类的全部成员(init/prop/func) |
|
| 检索示例代码 |
|
| Java 符号 → Cangjie 等价物 |
|
| 编译错误 → 相关 API + 示例 |
|
| 列出知识库中所有模块 |
|
| per-call 渐进式披露:拆成每个 API 调用,逐个锁类型+分层检索,返回 |
|
| 只生成 Java 代码的中英双语 NL 描述(不检索) |
|
快速开始
知识库数据(JSONL)已随仓库托管在 GitHub——clone 即用,不需要在本机重新构建。 BM25 索引(.pkl)是派生产物,首次运行时自动重建。
# 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 镜像运行:
# 构建镜像(打包源码 + 知识库 + 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:
// opencode.json(已随仓库提供)
{
"mcp": {
"cangjie-knowledge": {
"type": "local",
"command": ["docker", "run", "-i", "--rm", "cangjie-knowledge-mcp"],
"enabled": true
}
}
}若本机有 Python(无需 Docker),可改为:
{
"mcp": {
"cangjie-knowledge": {
"type": "local",
"command": ["python", "-m", "cjkb.mcp_server"],
"enabled": true
}
}
}注册后,agent 在片段翻译和错误修复时可以直接调用上述 9 个工具。
手动维护知识库(推荐工作流)
data/ 的 JSONL 托管进 git 的原因:JSONL 是人类可读、可 diff、可 review 的
源数据,pkl 是机器生成的派生品。维护流程:
# 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 位置填写:
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 的翻译流程为例,推荐的调用时机:
翻译前:把待翻译片段丢给
resolve_java_code→ 得到每个 API 调用的建议, 取 API 签名和示例注入 prompt;细节确认用get_class_members/java_to_cangjie/find_examples。错误修复循环(
cjpm build失败后):把编译错误传给error_fix_hint→ 返回相关 API 签名和示例,拼进错误反馈 prompt。类型解析 RAG 兜底:把
search_api结果作为额外证据注入类型映射 prompt。
说明:本项目是独立于 x2cangjie 的新项目,不修改 x2cangjie 的代码。接入方式: (a) 在 agent(MCP 客户端)侧注册上面的 server,让 agent 直接调用; (b) 在 x2cangjie 的 Python 代码中 import
cjkb直接调用Searcher(程序化 API)。
程序化调用(不经过 MCP)
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 用于四处:
NL 描述生成(
resolve_java_code/describe_java_code):LLM 把 Java 代码转成 中英双语 NL 描述。未配置时自动退回启发式(驼峰拆分 + 术语映射),检索不受影响。语义 rerank(
search_api/resolve_java_code等检索出口):LLM 对 BM25 top-k 重排。未配置或rerank=false时退回纯 BM25 顺序。补写缺失示例:为没有官方示例的 API 生成示例。
编译错误修复建议(
error_fix_hint):LLM 直接根据编译错误生成中文修复建议 (解释错误含义 + 修复方法)。未配置时退回关键词检索(见下文"变更记录")。
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:
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 索引
测试
# 单元测试 + 综合端到端测试(需 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(不再硬塞一个块级兜底建议)。