一饮
上次结束的基于typescript语言写的mcp工具太过潦草(这部分由于本人疏懒,便让 deepseek 代笔,仅仅大致勾勒了当日的日志,有所不到之处),因而重新起草,为其结尾。
提纲挈领地说,mcp项目的创建与typescript项目文件地创建有所互通。首先是搭建脚手架,这一部分我是由claude code代劳,原因是我是基于一个已有的项目进行的差异化复现,所以pakage.json这个文件基本上就能够敲定下来,然后根据pakage.json选择lst版本的tsconfig.json,整个配置文件如下:
1 | { |
一般情况下ts的工程文件并不需要我们手动写配置,我们可以使用一套默认配置。假设在主机上已经安装好Node.js和npm的情况下(tips:前者负责运行 npm、跑编译后的 JS。TS 本身不是浏览器/系统能直接跑的;后者负责装 TypeScript 编译器和各种包,类似于cmd指令中的install或者linux指令中的packman)
1 | npm init -y |
这样就可以一件创建好模板环境了,当然为了编译写好的文件还需要安装tsc,在已经安装过npm的情况下只需要
1 | npx tsc -v |
就可一看到弹出是否下载的指令,输入"y"就可以完成环境的安装。
一啄
然后我开始在claudecode的搭建的脚手架中设计整个 tool_kits 的功能,为了尝试给整个项目加上后端的内核,特意设计了一个简单的基于C++的算法(由于长久呆在力扣刷题的缘故,输入函数和输出函数均是两眼一摸黑,因为力扣只注重IPO中的Process。。。)。
然后mcp工具的模板在此就不再赘述了,毕竟本文只是对之前未到之处做补充… …
在此基础上我想补充我现阶段从模型训练侧到应用侧,优化或者约束输出的一点浅见:
在 mcp 的事告一段落之后,我把注意力转向了一个更成体系的题目:给明日方舟的 lore 语料搭一条本地 RAG 管线(LoreRAG),从采集、清洗、切块、向量化、建库,一路走到混合检索与生成。整条线跑下来,最想记下的倒不是哪个环节的成败,而是"约束输出"这件事,在模型侧与应用侧各自的分工。
模型侧
第一堵墙是显存。RTX 5070 Laptop 只有 8GB,嵌入模型 Qwen3-Embedding-4B 的 FP16 恰好要 8GB,放不进去;Q6_K 更小但有损;NVFP4 是 Blackwell 的新格式,偏速度向。最后落在 Q8_0(4.1GB,准无损)上。生成端同理,用的是 Qwen3-8B 的 Q4_K_M。严格说这次没有微调环节,训练侧能动的空间本就不大,真正可调的,是选型、量化和推理配置这些更靠"模型侧"的事。
服务进程由 llama.cpp 来起,编译 CUDA 后端时目标架构要盯死 sm_120(Blackwell)。嵌入服务里有一条参数不能错:
1 | llama-server --embeddings -m Qwen3-Embedding-4B-Q8_0.gguf --pooling last -ngl 99 --host 127.0.0.1 --port 8080 |
--pooling last 是 Qwen3-Embedding 要求的 last-token pooling——池化方式取错,2560 维的向量就只是排列整齐的数字垃圾;检索侧的 query 也要按官方推荐加指令前缀"为检索生成向量:{文本}",问题与文档才落进同一片向量空间。
模型侧还有一个反直觉的观察:31,287 块语料向量化时,批量提交并不比串行快(16 条长文本 4.06s ≈ 串行)。GPU 利用率 99%,功耗却只有 50W——单条前向被显存带宽卡死,算力是过剩的。(Os:好想有个好心人用RTX 6000 Pro砸在我身上,然后再用amd9800X3D和两根三星的64GB的内存条狠狠地羞辱我)
应用侧
模型侧决定输出能有多好,“不胡说"的底线却要靠应用侧一点一点砌。纯向量检索在这里暴露了两个短板:专有名词(如"整合运动”)语义漂移,容易召回泛泛的台词;voice 短台词占语料 58%,语义沾边却无信息量,把真正的 lore 答案淹没。于是检索改为两路召回加 RRF 融合:BM25 管词面,向量管语义,各取 top-60,按 1/(k+rank) 融合(k=60)。建库也有讲究——ChromaDB 得直接喂预计算向量,它默认的 all-MiniLM 只有 384 维,与本地 Qwen3 的 2560 维对不上。
融合之外再压三道:
- 类型加权:lore_term 1.2 > story/archive 1.0 > enemy 0.8 > voice 0.35。语义"沾边无料"的台词降权;BM25 侧不降——词面命中本身就是强信号。
- 术语精确命中置顶:query 里精确出现某个术语名,该术语的定义直接排到最前、不参与 RRF 排名——否则"矿石病是什么"会被三万个碎片淹没。
- 别名对齐:官方术语叫"矿石病",用户偏问"源石病"——检索前先把别名 OR 展开,喂给两路召回。
再往源头看,数据本身也有缺口。Kengxxiao 的语料里"矿石病"被提及了 1767 次,却没有一句"矿石病是……"的定义——定义类问题一旦让模型从碎片里归纳,就是幻觉的温床。于是手工 seed 了一份术语词典(14 条核心术语,分概念/组织/地理三类),以"直接答案"的身份进索引;PRTS wiki 被 WebFetch 的网络策略拦下(domain 无法验证),批量扩充的路暂时断着。检索中还揪出一批体检类模板文本——"造影检测结果显示……"词面语义双命中、却毫无 lore 信息,在 RRF 里专门淹没定义,于是回炉清洗:431 条纯数值的"综合体检测试"整条删,423 条模板首句剥离只留叙事,白面鸮还有一条"已为您检索出…"的变体要单独补剥。索引从 31,301 条瘦身到 30,870 条。
最后一道闸在生成侧。8B 模型再克制,也有归纳的冲动,prompt 于是写得像纪律条例:
1 | /no_think |
一条条写下来才回过味:约束输出不是给模型上枷锁,而是替它划一块够得着、也看得清的地盘——模型侧管上限,应用侧管下限,合起来才是可以信的回答。
所有的工作大体做的差不多了,思虑了两秒就想到了给我的本地模型套个deepseek harness,十分方便地将接口插入,然后后续收尾阶段,我将把loreRAG通过lore-mcp-server接入,这样我的loreRAG就能够正式收工了。
君悦
泽雉十步一啄、百步一饮,并不稀罕被养在樊笼里。我折腾这些,大概也只是想按自己的时令饮啄,然后向晦入宴,安然一息。