1. 为什么大段中文文本丢给 Codex 会“读偏”
我拿一份 1.8 万字的行业调研报告做过测试:直接把全文塞进对话,让它“提取关键词”,返回的结果里混着“我们”“可以”“进行”这类高频虚词,真正有价值的“向量检索”“冷启动”“召回率”反而被淹没。这不是模型不行,而是关键词抽取这件事本身不适合纯靠语义直觉完成——它需要词频统计、停用词过滤、权重排序这些确定性计算。
Codex Skills 的价值就在这里:它允许你把一段可复用的本地逻辑(比如中文分词 + TF-IDF)封装成技能,让 Codex 在需要时调用,而不是每次靠提示词“求”它认真读。你可以把它理解成给 AI 装了一个“外挂计算器”:语义理解交给模型,词频统计交给代码,各干各的强项。
这篇要解决的问题很具体:如何把 jieba 中文分词模块封装成一个 Codex Skill,让 Codex 在长文关键词抽取任务里稳定输出结构化结果。适合三类人:经常处理中文长文档的开发者、想给 Codex 扩展本地能力的工程师、以及被“关键词提取不准”折磨过的 NLP 初学者。
核心检索词先明确:Codex Skills 是 Codex 的扩展机制,中文智能分词模块负责把连续汉字切成词,关键词抽取则依赖 TF-IDF 或 TextRank 给词打分。三者串起来,就是一套可复现的长文本关键词流水线。
我试过最朴素的方案——写个 Python 脚本手动跑,每次都要切终端、改路径、复制结果回对话,来回折腾。封装成 Skill 之后,Codex 能直接调用,省掉大量搬运成本。下面从目录结构开始,一步步搭出来。
2. Codex Skills 目录结构与中文分词模块接入前置
在动手写代码前,先把 Codex Skills 的加载逻辑理清楚。Codex 识别 Skill 的方式是扫描约定目录下的描述文件,读取其中的名称、触发条件和执行入口。所以一个最小可用的 Skill 至少包含两部分:一份声明元信息的配置,一份真正干活的脚本。
2.1 Skill 目录长什么样
推荐的目录结构如下,放在 Codex 约定的 skills 根目录下:
skills/ └── cn-keyword-extract/ ├── skill.json # Skill 元信息与触发描述 ├── requirements.txt # Python 依赖 ├── extractor.py # 分词与关键词抽取主逻辑 └── stopwords.txt # 中文停用词表skill.json是入口,Codex 靠它判断“什么时候该调用这个技能”。extractor.py是实际执行体,接收文本、返回关键词列表。stopwords.txt单独放,方便你后续按领域增删。
2.2 依赖与运行环境准备
中文分词模块的核心依赖是 jieba,关键词权重计算用 scikit-learn 的 TfidfVectorizer,两者都是纯 Python 生态,安装成本低:
pip install jieba scikit-learn如果你打算把词云也纳入技能输出,再加一个 wordcloud 和 matplotlib。但关键词抽取本身不需要它们,建议先跑通最小闭环,可视化后面按需加。
注意:jieba 首次运行会构建前缀词典缓存,第一次调用会慢 1–2 秒,之后走缓存就很快。如果你在 Skill 里做超时控制,记得把这个冷启动时间算进去。
2.3 停用词表为什么必须单独维护
停用词是关键词抽取的“过滤器”。中文里“的、了、在、是、和”这类词出现频率极高,但对主题贡献几乎为零。如果不过滤,TF-IDF 排出来的前几名全是虚词,整个技能就废了。
我建议停用词表分两层:一层是通用停用词(几百个常见虚词),一层是领域停用词(比如你做电商分析,就把“商品”“用户”这类泛词也加进去)。stopwords.txt每行一个词,加载时按行读取即可。
2.4 把分词模块接入 Codex 的两种方式
第一种是脚本调用式:Skill 声明里写明执行命令,Codex 把待处理文本作为参数传入,脚本跑完返回 JSON。这种方式隔离性好,适合逻辑较重的场景。
第二种是函数注册式:如果你的 Codex 环境支持 Python 函数直接注册为工具,可以把extract_keywords(text)直接暴露出去,省掉进程启动开销。
两种方式我都试过,脚本调用式更稳,因为依赖和主进程隔离,不会因为 jieba 版本冲突影响 Codex 本体。下面统一按脚本调用式来写。
3. 可复制配置:skill.json 与 extractor.py 完整实现
这一节是全文的核心,所有代码都可以直接复制运行。先给配置文件,再给主逻辑,最后给停用词样例。
3.1 skill.json 声明文件
{ "name": "cn-keyword-extract", "version": "1.0.0", "description": "对中文长文本进行分词并抽取核心关键词,返回带权重的关键词列表", "trigger": { "keywords": ["提取关键词", "中文分词", "关键词抽取", "长文摘要"], "description": "当用户需要对大段中文文本提取关键词时调用" }, "entry": { "command": "python", "args": ["extractor.py", "--text", "{input}", "--topk", "15"] }, "input_schema": { "type": "object", "properties": { "text": { "type": "string", "description": "待处理的中文文本" }, "topk": { "type": "integer", "default": 15 } }, "required": ["text"] }, "output_schema": { "type": "object", "properties": { "keywords": { "type": "array" }, "method": { "type": "string" } } } }这里的关键字段是trigger.keywords,Codex 靠它匹配用户意图。entry里的{input}是占位符,Codex 会把实际文本替换进去。input_schema和output_schema让 Codex 知道怎么传参、怎么解析返回,避免格式对不上。
3.2 extractor.py 主逻辑
import argparse import json import os import jieba import jieba.analyse from sklearn.feature_extraction.text import TfidfVectorizer BASE_DIR = os.path.dirname(os.path.abspath(__file__)) STOPWORDS_PATH = os.path.join(BASE_DIR, "stopwords.txt") def load_stopwords(path): if not os.path.exists(path): return set() with open(path, "r", encoding="utf-8") as f: return {line.strip() for line in f if line.strip()} def preprocess(text, stopwords): words = jieba.cut(text, cut_all=False) kept = [w for w in words if len(w) > 1 and w not in stopwords and w.strip()] return " ".join(kept) def extract_tfidf(text, topk, stopwords): processed = preprocess(text, stopwords) if not processed: return [] vectorizer = TfidfVectorizer(stop_words=list(stopwords)) matrix = vectorizer.fit_transform([processed]) names = vectorizer.get_feature_names_out() scores = matrix.toarray()[0] pairs = sorted(zip(names, scores), key=lambda x: x[1], reverse=True) return [{"word": w, "score": round(float(s), 4)} for w, s in pairs[:topk]] def extract_textrank(text, topk): tags = jieba.analyse.textrank(text, topK=topk, withWeight=True) return [{"word": w, "score": round(float(s), 4)} for w, s in tags] def main(): parser = argparse.ArgumentParser() parser.add_argument("--text", required=True) parser.add_argument("--topk", type=int, default=15) parser.add_argument("--method", default="tfidf", choices=["tfidf", "textrank"]) args = parser.parse_args() stopwords = load_stopwords(STOPWORDS_PATH) if args.method == "tfidf": result = extract_tfidf(args.text, args.topk, stopwords) else: result = extract_textrank(args.text, args.topk) print(json.dumps({"keywords": result, "method": args.method}, ensure_ascii=False)) if __name__ == "__main__": main()这段代码有两个抽取入口:extract_tfidf走 sklearn 的向量化,适合有明确停用词表的场景;extract_textrank走 jieba 内置的图模型算法,不需要外部语料,适合单篇短文本。默认走 TF-IDF,因为长文场景下它的区分度更稳定。
3.3 stopwords.txt 样例
的 了 在 是 我 有 和 就 不 人 都 一 一个 上 也 很 到 说 要 去 你 会 着 没有 看 好 自己 这 我们 可以 进行 通过 以及通用停用词网上有很多现成版本,直接下载一份几百词的即可。关键是领域停用词要自己补:比如你做技术文档分析,就把“系统”“功能”“模块”这类泛词加进去,否则它们会挤占真正有区分度的词的位置。
3.4 参数对照表
| 参数 | 作用 | 推荐值 | 说明 |
|---|---|---|---|
| --text | 待处理文本 | 必填 | 长文建议先去除 HTML 标签 |
| --topk | 返回关键词数量 | 10–20 | 太少漏重点,太多掺噪音 |
| --method | 抽取算法 | tfidf | 单篇短文可切 textrank |
| cut_all | 分词模式 | False | 精确模式,保持语义完整 |
配置齐了,下一步就是验证它到底能不能跑通、结果对不对。
4. 验证请求:长文本关键词抽取结果对比
光有代码不算数,得拿真实长文本跑一遍,看输出是否符合预期。我准备了一段约 600 字的技术评论,分别用“纯 Codex 对话”和“Codex 调用 Skill”两种方式抽取关键词,对比差异。
4.1 构造测试文本
测试文本大意是讨论大模型在中文检索场景下的落地难点,包含“向量检索”“召回率”“冷启动”“语义漂移”等专业词,也夹杂大量“我们”“可以”“进行”这类虚词。这种文本最能暴露关键词抽取的质量差异。
4.2 调用 Skill 的命令
在 Codex 里触发技能后,实际执行的是这样一条命令:
python extractor.py --text "把测试文本粘贴到这里" --topk 12 --method tfidf返回结果是一个 JSON:
{ "keywords": [ {"word": "向量检索", "score": 0.4213}, {"word": "召回率", "score": 0.3876}, {"word": "冷启动", "score": 0.3542}, {"word": "语义漂移", "score": 0.3218}, {"word": "大模型", "score": 0.2987} ], "method": "tfidf" }4.3 两种方式结果对比
| 对比项 | 纯 Codex 对话 | Codex 调用 Skill |
|---|---|---|
| 前 5 关键词 | 混入“我们”“可以” | 全是专业术语 |
| 权重可解释性 | 无分数 | 每词带 TF-IDF 分数 |
| 结果稳定性 | 每次略有不同 | 同输入同输出 |
| 处理耗时 | 依赖模型推理 | 毫秒级本地计算 |
| 长文适配 | 易被上下文截断 | 不受窗口限制 |
实测下来,Skill 方式的前 5 关键词全部命中文本核心主题,而纯对话方式至少有两个虚词混入。差距的根源不在模型能力,而在于词频统计这件事本就该用确定性算法做。
4.4 结果解读与调参
如果你发现返回的关键词里还有泛词,说明停用词表不够全,往stopwords.txt里补即可。如果发现专业术语被切碎了(比如“向量检索”被切成“向量”和“检索”),说明 jieba 词典没收录这个词,用jieba.load_userdict()加载自定义词典就能解决。
调参的核心就两个旋钮:topk控制数量,停用词表控制质量。先把停用词调干净,再调 topk,顺序别反。
5. 本篇常见错排查:401、local proxy failed 与 reading choices
技能跑不起来,八成是下面几类错误。我按真实报错逐条给排查路径。
5.1 401 未授权
如果你在 Skill 里调用了远程模型接口,报 401 通常是 Key 没配对或没带上。检查三件套是否齐全:Base URL、API Key、Model ID。以 TaoToken 为例,Base URL 填https://taotoken.net/api,Key 在控制台的 API Keys 页面生成,Model ID 按你实际使用的模型填写。三者缺一不可,且 Key 不要有多余空格。
5.2 local proxy failed
这个报错一般出现在网络请求环节,说明请求没发出去就被拦了。先确认你的运行环境网络正常,再检查 Skill 配置里有没有写死某个不可达的地址。如果是本地脚本调用远程接口,把 Base URL 换成可访问的地址即可。注意不要在任何配置里写代理相关字段,保持直连。
5.3 reading choices 报错
这个错误通常出现在解析模型返回时——代码期望拿到choices字段,但实际返回结构不是标准格式。排查两步:第一,打印原始返回体,看结构长什么样;第二,确认你用的接口路径和模型是否匹配。如果是 OpenAI 兼容格式,返回里应该有choices[0].message.content。结构对不上,多半是 Base URL 或 Model ID 填错了。
5.4 OAuth 相关报错
如果你用的是需要 OAuth 的编码工具(比如 Claude Code 这类),报 OAuth 错误通常是认证流程没走完或 token 过期。重新走一遍授权流程,确认回调地址和配置一致。如果工具支持 API Key 方式,直接切到 Key 认证更省事,避免 OAuth 的来回跳转。
5.5 分词结果为空
如果 Skill 返回空列表,先检查输入文本是不是被 shell 转义吃掉了。命令行传中文时,建议用文件方式传入而不是直接拼在--text后面。另外确认stopwords.txt没有把有效词也过滤掉——停用词表写太狠,会把正常词也干掉。
5.6 中文乱码
Windows 环境下命令行默认编码可能是 GBK,导致中文输出乱码。在脚本开头加sys.stdout.reconfigure(encoding='utf-8'),或者运行时设置环境变量PYTHONIOENCODING=utf-8。这个坑很常见,但解决起来一行代码的事。
6. 把技能用起来:从单次抽取到长期工作流
跑通单次抽取只是起点。真正省时间的是把它接进日常工作流:比如你每天要处理一批行业报告,可以写个批处理脚本,遍历目录下所有 txt,逐个调用extractor.py,把结果汇总成一张关键词表。Codex 负责调度和结果解读,Skill 负责确定性计算,分工明确。
如果你打算长期做编码类、Agent 类的任务,建议把这类本地技能和 Coding Plan 结合起来用,让 Codex 在长任务里稳定调用本地能力,而不是每次重新推理。模型对话入口适合快速验证单个模型的关键词抽取效果,接入文档则能帮你把 Base URL、Key、Model ID 这套配置一次配对,少走弯路。
最后留一个实用技巧:把stopwords.txt纳入版本管理,每次发现新的泛词就补进去。这个文件会随着你的使用越来越准,几个月后它就是你这套技能最值钱的资产——因为它记录的是你所在领域的“废话清单”。