关于 Typecast
Typecast 面向评估 LLM 提取流程的开发者,可将职位描述转换为经过架构验证的 JSON,并通过错误反馈重试及实验结果衡量有效性和准确度。
主要功能
- 01根据模型架构验证 JSON
- 02错误反馈重试,最多三次
- 03确定性整理技能和地点
- 04统计模型有效性与准确度
适用场景
- 01从职位描述提取结构化数据
- 02比较架构约束与重试策略
- 03衡量多轮运行中的提取失败
MCP 相关
项目在 PyPI 提供 `typecast-mcp` MCP 服务;输入未说明其 MCP 接口。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
2收录的工具列表 · 更新于 3 天前
此工具无需参数。
详细介绍
Typecast
能将杂乱的职位描述转换为通过架构验证的类型化 JSON,并衡量转换失败的频率的 LLM Agent。
主要发现(通过 OpenRouter 使用两种 Qwen 模型处理 30 条真实职位描述):
- 架构模式能让输出有效,但不保证正确。 在设置 C 中,两种模型的输出有效率均为 98.9%,成本低于重试,但端到端准确率分别为 67.1%(7B)和 84.7%(235B)。
- 错误反馈重试修复了小模型的问题,却没有修复大模型的问题。 设置 B 中,7B 模型的有效率为 98.9%,而 235B 模型为 88.9%;后者在四次设置 B 运行中都未能处理 jd_002。加入考虑数量的重试消息也没有改变这一结果。
- 没有一次运行达到我 90% 的字段准确率目标(最佳:87.3%),而且不同运行的结果会有差异。 同一设置的端到端准确率在不同运行间相差约 8 个百分点,因此,单次运行之间的差距不能作为定论。
60 秒演示:
https://github.com/user-attachments/assets/c4a2572c-88a2-49a3-8e20-176010877154
CLI 和包名称:soa · PyPI 上的 MCP 服务:typecast-mcp
结果
通过 OpenRouter 使用两种开放权重模型,处理 30 条真实职位描述,共进行四种设置。字段准确率根据 20 条人工核验的标准标签评分(标签的制作方式见局限性)。进行 3 次运行时,数值为平均值,括号内为各次运行的最小值至最大值;进行 1 次运行时,数值即为该次运行结果。A = 普通提示词,不重试;B = 普通提示词 + 错误反馈重试;C = 提供商架构模式,不重试;D = 架构模式 + 重试。
| 模型 | 设置 | 运行次数 | 最终有效率 | 端到端准确率 | 每次处理 30 条文档的成本 |
|---|---|---|---|---|---|
| qwen-2.5-7b-instruct | A · 普通 | 1 | 83.3% | 50.4% | $0.0081 |
| qwen-2.5-7b-instruct | B · 普通 + 重试 | 3 | 98.9% (96.7%-100.0%) | 67.1% (62.9%-70.4%) | $0.0103 ($0.0101-$0.0105) |
| qwen-2.5-7b-instruct | C · 架构 | 3 | 98.9% (96.7%-100.0%) | 67.1% (65.0%-69.8%) | $0.0076 ($0.0076-$0.0076) |
| qwen-2.5-7b-instruct | D · 架构 + 重试 | 1 | 100.0% | 70.0% | $0.0080 |
| qwen3-235b-a22b-2507 | A · 普通 | 1 | 70.0% | 60.4% | $0.0120 |
| qwen3-235b-a22b-2507 | B · 普通 + 重试 | 3 | 88.9% (86.7%-93.3%) | 75.6% (72.8%-80.9%) | $0.0211 ($0.0199-$0.0234) |
| qwen3-235b-a22b-2507 | C · 架构 | 3 | 98.9% (96.7%-100.0%) | 84.7% (82.8%-85.8%) | $0.0136 ($0.0134-$0.0140) |
| qwen3-235b-a22b-2507 | D · 架构 + 重试 | 1 | 100.0% | 85.4% | $0.0143 |
完整表格:还包含首次尝试有效率、平均调用次数和字段准确率
| 模型 | 设置 | 运行次数 | 首次尝试有效率 | 最终有效率 | 平均调用次数 | 字段准确率(有效输出) | 端到端准确率 | 每次 30 份文档运行的成本 | |---|---|---|---|---|---|---|---|---| | qwen-2.5-7b-instruct | A:普通提示词,不重试 | 单次运行 | 83.3% | 83.3% | 1.00 | 62.9% | 50.4% | $0.0081 | | qwen-2.5-7b-instruct | B:普通提示词 + 错误反馈重试 | 3 次运行的均值(最小值–最大值) | 82.2%(80.0%-83.3%) | 98.9%(96.7%-100.0%) | 1.20(1.17-1.23) | 68.2%(66.0%-70.4%) | 67.1%(62.9%-70.4%) | $0.0103($0.0101-$0.0105) | | qwen-2.5-7b-instruct | C:模式,不重试 | 3 次运行的均值(最小值–最大值) | 98.9%(96.7%-100.0%) | 98.9%(96.7%-100.0%) | 1.00(1.00-1.00) | 68.2%(66.5%-69.8%) | 67.1%(65.0%-69.8%) | $0.0076($0.0076-$0.0076) | | qwen-2.5-7b-instruct | D:模式 + 重试 | 单次运行 | 96.7% | 100.0% | 1.03 | 70.0% | 70.0% | $0.0080 | | qwen3-235b-a22b-2507 | A:普通提示词,不重试 | 单次运行 | 70.0% | 70.0% | 1.00 | 86.6% | 60.4% | $0.0120 | | qwen3-235b-a22b-2507 | B:普通提示词 + 错误反馈重试 | 3 次运行的均值(最小值–最大值) | 76.7%(73.3%-80.0%) | 88.9%(86.7%-93.3%) | 1.34(1.33-1.37) | 85.6%(85.1%-86.0%) | 75.6%(72.8%-80.9%) | $0.0211($0.0199-$0.0234) | | qwen3-235b-a22b-2507 | C:模式,不重试 | 3 次运行的均值(最小值–最大值) | 98.9%(96.7%-100.0%) | 98.9%(96.7%-100.0%) | 1.00(1.00-1.00) | 86.1%(85.6%-87.0%) | 84.7%(82.8%-85.8%) | $0.0136($0.0134-$0.0140) | | qwen3-235b-a22b-2507 | D:模式 + 重试 | 单次运行 | 100.0% | 100.0% | 1.00 | 85.4% | 85.4% | $0.0143 |如何解读:
- “3 次运行的均值(最小值–最大值)”(B 和 C)表示在相同的 30 份文档上运行 3 次的均值,括号内为最低和最高运行结果。这反映的是多次运行间的差异,并非区间估计。B 的运行是下文所述重试反馈修复后进行的 3 次运行。C 的运行包括首次 C 运行以及两次重复运行。“单次运行”(A 和 D)各指一次抽样:重复运行显示,同一设置在不同运行间的端到端准确率会波动约 8 个百分点(235B 设置 B:72.8% 到 80.9%)。每次运行自身的 95% 区间(有效率列采用 Wilson 区间,准确率列采用 bootstrap)见本节末尾的逐次运行表;A 和 D 的单次运行没有区间,因为进行这些运行时,区间计算代码尚不存在。
- 首次尝试有效率 / 最终有效率:30 份文档中,首次回复通过验证的比例,或在最多 3 次尝试内有任意回复通过验证的比例。平均调用次数:每份文档的模型调用次数。
- **字段准确率(有效输出)**仅在产生有效 posting 的文档上测量,因此会让经常失败的设置看起来表现更好(235B 设置 A:字段准确率为 86.6%,但有有效输出可评分的标注文档仅占 70.0%)。端到端准确率会将失败的文档的每个字段都计为错误;比较有效率不同的设置时,应以此为准。
- 成本是提供商报告的 30 份文档单次运行成本。未设置 temperature(使用提供商默认值,具体值未知)。
我的目标及达成情况
运行前,我为设置 B 设定了三个目标:最终有效率达到 30/30、平均每份文档最多调用 1.3 次,以及字段准确率至少达到 90%。各次运行的结果如下(重试修复后的 3 次运行):
| 目标(设置 B) | qwen-2.5-7b-instruct | qwen3-235b-a22b-2507 |
|---|---|---|
| 最终有效率 30/30 | 3 次运行中有 2 次达成(100.0%、100.0%、96.7%) | 3 次全部未达成(86.7%、93.3%、86.7%) |
| 平均调用次数 <= 1.3 | 达成(1.20、1.17、1.23) | 未达成(1.37、1.33、1.33) |
| 字段准确率 >= 90% | 未达成(70.4%、68.1%、66.0%) | 未达成(85.7%、85.1%、86.0%) |
两种模型在所有设置下的每次运行都未达到 90% 的字段准确率目标。所有结果中最高的是 87.3%(235B,设置 B,首轮运行)。
每次运行及其 95% 区间(results/summary.md,原文)
第 1–8 行:第一轮,每个模型和配置运行一次(7B 的 A、B、C、D,然后是 235B)。这些数据是在区间代码编写完成前记录的,因此没有区间。第 9–18 行:重复轮次,依次为 7B B ×3、7B C ×2、235B B ×3、235B C ×2。区间以方括号标出。列说明:`field acc` 为有效输出中的准确率,`coverage` 为 20 份已标注文档中有有效输出的占比,`e2e acc` 将失败计为错误。| 模型 | 配置 | n | 首次尝试 | 最终 | 平均调用次数 | 字段准确率(规范化) | 覆盖率 | 端到端准确率(规范化) | 成本 |
|---|---|---|---|---|---|---|---|---|---|
| openrouter/qwen/qwen-2.5-7b-instruct | A | 30 | 83.3% | 83.3% | 1.00 | 62.9% | 80.0% | 50.4% | $0.0081 |
| openrouter/qwen/qwen-2.5-7b-instruct | B | 30 | 86.7% | 96.7% | 1.17 | 68.3% | 95.0% | 64.8% | $0.0102 |
| openrouter/qwen/qwen-2.5-7b-instruct | C | 30 | 100.0% | 100.0% | 1.00 | 69.8% | 100.0% | 69.8% | $0.0076 |
| openrouter/qwen/qwen-2.5-7b-instruct | D | 30 | 96.7% | 100.0% | 1.03 | 70.0% | 100.0% | 70.0% | $0.0080 |
| openrouter/qwen/qwen3-235b-a22b-2507 | A | 30 | 70.0% | 70.0% | 1.00 | 86.6% | 70.0% | 60.4% | $0.0120 |
| openrouter/qwen/qwen3-235b-a22b-2507 | B | 30 | 83.3% | 86.7% | 1.33 | 87.3% | 85.0% | 74.3% | $0.0186 |
| openrouter/qwen/qwen3-235b-a22b-2507 | C | 30 | 100.0% | 100.0% | 1.00 | 85.8% | 100.0% | 85.8% | $0.0140 |
| openrouter/qwen/qwen3-235b-a22b-2507 | D | 30 | 100.0% | 100.0% | 1.00 | 85.4% | 100.0% | 85.4% | $0.0143 |
| openrouter/qwen/qwen-2.5-7b-instruct | B | 30 | 80.0% [62.7-90.5] | 100.0% [88.6-100.0] | 1.20 | 70.4% [64.4-76.9] | 100.0% | 70.4% [64.4-76.9] | $0.0104 |
| openrouter/qwen/qwen-2.5-7b-instruct | B | 30 | 83.3% [66.4-92.7] | 100.0% [88.6-100.0] | 1.17 | 68.1% [61.4-74.7] | 100.0% | 68.1% [61.4-74.7] | $0.0101 |
| openrouter/qwen/qwen-2.5-7b-instruct | B | 30 | 83.3% [66.4-92.7] | 96.7% [83.3-99.4] | 1.23 | 66.0% [59.6-72.2] | 95.0% | 62.9% [54.5-70.5] | $0.0105 |
| openrouter/qwen/qwen-2.5-7b-instruct | C | 30 | 100.0% [88.6-100.0] | 100.0% [88.6-100.0] | 1.00 | 66.5% [59.6-73.8] | 100.0% | 66.5% [59.6-73.8] | $0.0076 |
| openrouter/qwen/qwen-2.5-7b-instruct | C | 30 | 96.7% [83.3-99.4] | 96.7% [83.3-99.4] | 1.00 | 68.4% [61.3-75.0] | 95.0% | 65.0% [55.2-73.3] | $0.0076 |
| openrouter/qwen/qwen3-235b-a22b-2507 | B | 30 | 76.7% [59.1-88.2] | 86.7% [70.3-94.7] | 1.37 | 85.7% [81.3-89.6] | 85.0% | 72.8% [58.4-84.8] | $0.0234 |
| openrouter/qwen/qwen3-235b-a22b-2507 | B | 30 | 73.3% [55.6-85.8] | 93.3% [78.7-98.2] | 1.33 | 85.1% [81.1-88.9] | 95.0% | 80.9% [71.1-87.9] | $0.0199 |
| openrouter/qwen/qwen3-235b-a22b-2507 | B | 30 | 80.0% [62.7-90.5] | 86.7% [70.3-94.7] | 1.33 | 86.0% [82.0-89.6] | 85.0% | 73.1% [58.7-84.9] | $0.0202 |
| openrouter/qwen/qwen3-235b-a22b-2507 | C | 30 | 96.7% [83.3-99.4] | 96.7% [83.3-99.4] | 1.00 | 87.0% [83.7-89.9] | 95.0% | 82.8% [72.3-89.0] | $0.0136 |
| openrouter/qwen/qwen3-235b-a22b-2507 | C | 30 | 100.0% [88.6-100.0] | 100.0% [88.6-100.0] | 1.00 | 85.6% [82.0-88.8] | 100.0% | 85.6% [82.0-88.8] | $0.0134 |
主表中的 A 和 D 数据对应上方第 1–8 行(A 和 D)。第一轮的 B 行(7B:86.7% / 96.7%,235B:83.3% / 86.7%,分别为首次尝试 / 最终)来自修复重试逻辑之前的运行;第一轮的 C 行是主表中三次 C 运行中的第一次。重复运行的汇总见 results/aggregate.md;每次运行的原始报告见 results/2026-11-04_*.json。
text ──► build prompt (schema + instructions)
│
▼
LLM call ──► raw string
│
▼
JobPosting.model_validate_json(raw)
│ ok │ ValidationError
▼ ▼
log success log failure (error + raw)
return model attempt < MAX? ──► append error to messages, retry
│ no
▼
raise ExtractionFailed (logged)
向模型展示的架构由 Pydantic 模型(JobPosting.model_json_schema())生成,从不手动编写,回复也使用同一个模型进行检查。若回复未通过验证,就会将验证器的错误反馈给模型,每个错误占一行,最多重试 3 次。得到有效回复后,两个确定性工具会整理结果(规范化技能名称,并根据文档标题检查地点)。它们是普通代码,并非模型调用,因此不会增加 LLM 调用次数,下面的各个设置也仍可进行比较。详情请参阅:docs/ARCHITECTURE.md。
四种设置:A 普通提示词,不重试。B 普通提示词,将错误反馈给模型(最多尝试 3 次)。C 提供商的严格 JSON Schema 模式,不重试。D Schema 模式加重试。
为什么不直接使用原生结构化输出?
Schema 模式帮助很大,而且成本更低,但它并不能完成全部工作。
它解决了什么问题。 对两种模型而言,设置 C 首次尝试即通过验证的比例为 98.9%(96.7%-100.0%);相比之下,单次普通提示词运行(A)中,7B 模型的比例为 83.3%,235B 模型为 70.0%。在普通模式下,模型实际违反的限制是 skills 最多 30 项;在 Schema 模式下,报告统计的是违反 Schema 所包含限制的回复,而每次 C 和 D 运行中此类回复均为 0(6 次 C 运行,2 次 D 运行)。C 的成本也低于 B:7B 模型为 $0.0076,而 B 为 $0.0103;235B 模型为 $0.0136,而 B 为 $0.0211(每次运行的平均值)。
它无法做到什么:
- 它只能保证结构,不能保证含义。 C 中剩余的失败属于 JSON Schema 无法表达的规则。6 次 C 运行中,有两次运行各有一份文档因技能名称重复而被拒绝(“LangChain”和“Langchain”;“Python”出现两次;两次拒绝均记录在 results/failure_examples.jsonl 中),因此 C 的最终有效率为 98.9%(96.7%-100.0%),而不是 100%。实习生意味着没有工作经验这条规则也有同样的问题(无法在 Schema 中声明);本项目记录的 637 行尝试记录中,这条规则从未触发(文件最后一行记录了该计数),因此我没有证据判断它是否有效。
- 有效不代表正确。 Schema 模式下,235B 模型的有效率为 98.9%(96.7%-100.0%),字段准确率为 86.1%(85.6%-87.0%),端到端准确率为 84.7%(82.8%-85.8%):低于我设定的 90% 目标。C 中的 7B 模型有效率同样高,但字段准确率仅为 68.2%(66.5%-69.8%);其资历级别准确率在 3 次 C 运行中分别为 55.0%、55.0% 和 52.6%。有效率并不能说明资历级别是否正确。
- 它可能以召回率换取有效性。 模型通过输出更短的技能列表来遵守数量上限,而技能恰恰是准确性最弱的字段。对于 235B 模型,3 次 C 运行中的技能 F1 分别为 66.9%、72.1% 和 65.7%;3 次 B 运行中则分别为 71.4%、71.6% 和 68.3%(平均值为 68.2% 和 70.4%,范围有重叠)。在第一轮单次运行中,A 和 B 分别为 72.1% 和 73.8%,C 和 D 则分别为 66.9% 和 63.4%。重复运行表明这是一种趋势,而非明确的效果;标注文档只有 n=20 份,我不会做出更多推断。
- 它取决于提供商。 两种模型在此都接受了严格的
json_schema,但这是因为请求使用了 OpenRouter 的require_parameters,确保请求只能路由到支持该功能的提供商。7B 模型始终由同一家提供商提供服务;235B 模型则由三家提供商提供服务。
最常见的失败类型及其修复方法
根据普通提示词运行和重试运行(计数为首次错误出现在相应字段的失败尝试数,
见报告中的 failures.by_first_loc):
| 失败 | 设置 A(运行 1 次) | 设置 B,运行 3 次 | 修复方法 |
|---|---|---|---|
skills 超过 30 项上限 | 7B:5 份失败文档中有 3 份;235B:9 份失败文档中有 8 份 | 7B:失败尝试数分别为 5、5、3;235B:分别为 14、11、14 | 重试反馈可修复大部分问题。Schema 模式可消除此问题。 |
技能名称超过 40 个字符(skills.*.name) | 7B 为 2,235B 为 1 | 7B:0、0、3;235B:1、1、0 | 重试 |
用 null 代替 true/false(skills.*.required、salary_mentioned) | 无 | 7B:一次运行中有 2 次尝试出错于 skills.*.required,另一次运行中有 1 次出错于 salary_mentioned | 重试 |
技能名称重复(也计入 skills,因为错误会报告在该字段) | 未单独计数 | 未单独计数 | 重试可修复:设置 B 中所有 5 次因名称重复而被拒的结果,在下一次尝试时均有效(每个被拒的尝试及其有效后续结果都记录在 results/failure_examples.jsonl 中)。Schema 模式无法修复:这是设置 C 中唯一剩下的失败(两份文档,同一文件)。 |
| JSON 格式错误、Markdown 围栏、说明文字、额外字段、无效枚举值 | 无:没有任何运行在根级字段、seniority、mode 或其他枚举值或额外字段上失败 | 无 | 无需修复 |
几乎所有问题都是数量上限,而非格式问题。在最初包含 11 份文档的基线测试中,两个模型的所有失败都是 skills 列表过长(results/2026-11-03_baseline_*.md)。我首先尝试让反馈更加具体。默认反馈行使用验证器自身的措辞(例如 skills: List should have at most 30 items after validation, not 42)。对于过长列表,我将其替换为包含数量和修复建议的反馈,例如 skills: has 39 items, maximum is 30 - remove 9, starting with required=false ones。结果好坏参半:
- 这没有提高设置 B 中 235B 的最终有效率:修改前为 86.7%,之后三次运行分别为 86.7%、93.3% 和 86.7%。每次运行的区间都包含原来的数值,因此在 n=30 时看不出效果。
- 在之后的 12 次文档运行中,首次 B 运行里失败的 4 份 235B 文档(jd_002、jd_003、jd_005、jd_023)有 6 次通过(jd_002 为 0/3,jd_003 为 1/3,jd_005 为 2/3,jd_023 为 3/3),但其他文档(jd_029、jd_030、jd_004)转而失败。我无法将修复效果与运行间波动区分开来。修复前后的首次尝试完全相同,但首次尝试有效率从 83.3% 变为 76.7%、73.3% 和 80.0%;这种差异属于运行间波动(抽样以及第一轮测试中未记录的提供商组合)。
- **jd_002 在设置 B 的四次运行中对 235B 全部失败。**所有尝试都记录在 results/failure_examples.jsonl 中:首次运行(修复前)分别为 42、31、31 个技能;之后的三次运行分别为 70、31、31 个、47、31、31 个,以及 44、31、31 个。模型在首次重试时将技能数从 42–70 降至 31,然后就停在 31。对于包含 31 项的回复,新的反馈行是
skills: has 31 items, maximum is 30 - remove 1, starting with required=false ones(由代码生成;提示词文本本身未记录)。这是最明显的未解决失败:模型没有完成最后一步删减。
设计决策
- 每个模型都设置
strict=True。 Pydantic 的默认宽松模式会悄悄将"3"转成3,将"yes"转成True,因此草率的回复也能通过校验,重试循环无事可做。严格模式则会拒绝这两种情况。 - 字段必填但允许为 null(
company、city、country、min_years_experience)。不设置默认值:模型必须 明确写出null,因此“未提及”是模型作出的判断,而遗漏键则会报错,而不是静默地变成null。 - 重试提示词中每个错误占一行,格式为
loc: msg (got: <input, clipped to 60 chars>),根据ValidationError.errors()生成,而非使用str(error)。原始字符串包含随版本变化的前缀、截断后的input_value和文档 URL,这些对模型而言都是噪声。唯一例外是列表过长的情况,会使用上文所示的、包含数量信息的行。我没有运行盲目重试对照组(不提供错误文本直接重试),因此没有 衡量反馈相比直接重新采样的效果;设置 A 与设置 B 的对比是有反馈的重试与不提供反馈的对比。 - 工具用于确定性的后处理,不由模型调用。 验证通过后,再以普通代码运行规范技能名称处理和地点检查。这样每种设置都只需一个提示词,重试、尝试次数和成本统计也保持清晰;同时保留模型自己的输出(
raw_posting), 因此有效性是根据模型实际说出的内容来衡量的。让模型调用工具是另一个我尚未进行的实验。 - API 错误与验证错误分开处理。 429 或 5xx 错误会按退避间隔(2、4、8 s)重试,不会占用 3 次验证尝试中的任何一次。
- 保留 30 个技能的上限。 首次使用普通提示词进行基准测试时,所有失败都由这一条限制造成,因此我保留它,用来测试重试和 schema 模式能做到什么,而不是放宽限制让数字看起来更好。
局限性
- 30 份职位描述,20 个黄金标签。 一份文档会让有效率变动 3.3 个百分点,让字段准确率变动 5 个百分点;因此上面的区间较宽(例如,235B 配置 B 在一次运行中的最终有效率为 93.3% [78.7-98.2])。其中 5 个是非技术或半技术岗位,特意保留作为域外样本;其中两个(jd_012、jd_019)包含在 20 份已标注文档中。
- 黄金标签由模型(Claude)起草,然后由我检查。 我核实了每个字段,并做了 17 处修正(docs/label_corrections.log)。只有一个人检查了一遍,因此我犯下的任何系统性错误都会留在标签中。首次基线测试后,我将标注规则写入了 schema 的字段描述,因此模型和标签遵循相同规则;使用早期提示词进行的运行无法比较。
- 准确率的噪声下限,来自盲测。 对另外 5 份文档(jd_004、005、008、014、015,均不在黄金数据集中),我进行了盲标,并与 Claude 预先编写并封存的标签进行了比较,使用与评估相同的逐字段指标(以我的标签为参考):职位名称 80.0%,公司 100.0%,资历级别 80.0%,工作模式 80.0%,是否提及薪资 100.0%,最低工作年限 100.0%,城市 80.0%,国家 100.0%,技能 F1 61.0%(精确率 52.9%,召回率 72.0%),总体 86.8%(results/label_agreement.md)。5 份文档只能用于合理性检查,不能作为比率:一次意见不一致就会让某个字段变动 20 个百分点。这确实表明,在技能标注上,两位认真标注者之间的分歧,与模型和黄金标签之间的分歧程度相当;而 235B 大约 86% 的字段准确率,也接近两位标注者彼此之间的水平,因此距离我设定的 90% 目标仍有差距,其中一部分可能源于标签噪声,而非模型错误。但我尚未证明这一点。
- 所有运行都在同一天进行,使用同一系列的两个模型和同一供应商层。 未设置温度。7B 始终路由到一个供应商;235B 路由到三个供应商。第一轮未记录供应商,因此供应商组合和采样噪声都混入了各次运行之间的差异。
- 后处理工具没有针对最终标注规则进行调优,因此位置检查结果可能与某些文档的标签不一致。
- D 配置对每个模型只运行了一次;只有 B 和 C 重复运行。
- 数据。
data/jds/中的 30 份职位描述复制自公开招聘信息(LinkedIn 13 份、Work at a Startup 11 份、Wellfound 6 份,数量根据data/jds/manifest.csv统计),仅用于评估;如有要求,我会删除其中任何文档。manifest.csv中有 5 行未记录来源 URL:jd_001、jd_002、jd_003、jd_004 和 jd_010(均来自 LinkedIn;备注写着“未记录 URL”)。
自行运行
需要 Python 3.11+ 和 uv。
git clone https://github.com/nilayp-codes/typecast.git
cd typecast
uv sync
cp .env.example .env # then set OPENROUTER_API_KEY in .env (never commit it)
uv run soa extract data/jds/jd_001.txt # the JSON on stdout, a status line on stderr
uv run soa extract data/jds/jd_001.txt --json-mode # provider schema mode
uv run soa extract data/jds/jd_001.txt > out.json
uv run soa eval --model openrouter/qwen/qwen-2.5-7b-instruct --setup B --limit 5
uv run soa eval --model openrouter/qwen/qwen3-235b-a22b-2507 --setup all
已知问题:在 Windows 上,请克隆到较短的路径下,因为 LiteLLM 会在 .venv 下写入缓存文件,而过深的路径会超出 Windows 的路径长度限制(此时 soa extract 会报错 APIConnectionError ... [Errno 2])。
soa eval 会调用真实模型:它会显示计划和预计调用次数,并在发送任何内容前询问(--yes 可跳过询问)。在我的运行中,对 30 份文档使用一个配置的费用在 $0.0076 到 $0.0234 之间。它会保存 results/<date>_<model>_<setup>.json,并向 results/summary.md 追加一行;--resume <eval id> 可继续被中断的运行。要合并重复运行的结果:uv run python scripts/aggregate_runs.py results/<files>.json(会写入 results/aggregate.md)。30 份职位描述位于 data/jds/,黄金标签位于 data/labels/。
测试和类型检查(测试中不会调用真实模型):
uv run pytest
uv run mypy --strict src/ packages/typecast-mcp/src/
在 AI Agent 中使用(MCP 服务)
typecast-mcp 是一个轻量的 MCP 服务,可根据 JobPosting 架构检查职位发布 JSON,并返回 Agent 可以处理的单行错误。它不包含大模型,不需要 API 密钥,也不会发起网络调用。它是一个独立的软件包(packages/typecast-mcp/),依赖项只有 pydantic 和 mcp,因此安装时不会引入 LiteLLM 或其余评测框架。
提供两个工具:
get_job_posting_schema:职位发布的 JSON 架构。它不包含所有规则(重复技能名称和实习生/经验规则仅由验证器检查)。validate_job_posting:接受一个参数posting,即作为一个字符串传入的完整职位发布 JSON。返回valid、错误行列表(field: problem (got: value)),以及验证通过时解析后的职位发布内容。
安装(需要 uv)。适用于 Claude Desktop、Cursor 和其他客户端:
{
"mcpServers": {
"typecast": {
"command": "uvx",
"args": ["typecast-mcp"]
}
}
}
Claude Code:claude mcp add typecast -- uvx typecast-mcp。在 Windows 上,如果客户端找不到 uvx,请在 command 中填写 uvx.exe 的完整路径(使用 where.exe uvx 查找)。Microsoft Store 版 Claude Desktop 不会读取 %APPDATA%\Claude\claude_desktop_config.json;请打开 Settings → Developer → Edit Config,以打开它实际读取的文件。不使用 uv 时:运行 pip install typecast-mcp,并将 typecast-mcp 用作命令。
从克隆的仓库运行。 仓库自带 .mcp.json,可从工作树运行服务(uv run typecast-mcp)。在 Claude Code 中打开文件夹,出现提示时批准 typecast,然后用 /mcp 检查。批准后会在此文件夹中运行 uv run,安装锁定的依赖项并启动服务。请仅在你信任的克隆仓库中批准。
实际示例。 Qwen3-235B 对 jd_002 的回复包含 70 项技能。Agent 使用 validate_job_posting 检查后,收到一行错误:
skills: has 70 items, maximum is 30 - remove 40, starting with required=false ones
Agent 一轮就修正了职位发布内容。这是一次使用 Claude Sonnet 5.5 的运行,并非基准测试。
Claude Desktop 演示(演示中预先植入了错误)。Claude 调用 validate_job_posting,收到两行错误,修正后重新验证。请注意,为了让职位发布通过验证,它必须猜测年限:
https://github.com/user-attachments/assets/4190ef12-fc7c-46f8-ad48-dd128d80aa54
它不会检查的内容: 值是否正确。职位发布可以通过验证,但资历级别或技能仍然可能有误;请参阅为什么不直接使用原生结构化输出?。
详细信息、限制和设计决策:docs/MCP.md。
接下来我会做什么
仅用于验证的 MCP 服务:已完成,参见在 AI Agent 中使用。发布到 PyPI:MCP 服务已完成,typecast-mcp。- 修复数据指出的剩余失败:例如,解决无法通过 jd_002 最后一轮筛选的模型问题,告诉它应删掉哪些项目。固定温度并指定提供方后重新测量,以减少重复运行之间的差异。
- 为黄金集增加第二位标注者,扩大文档集,并重复运行设置 D,以免字段准确率数字受 n=20 限制。
许可证
MIT。参见 LICENSE。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。