1. 临床数据程序员校验 SDTMIG 3.2 的真实痛点
如果你正在做临床数据递交,SDTMIG 3.2 的域模型变量校验大概率是你绕不开的一关。DM 域里 AGE、SEX、RACE 这些记录修饰语到底该不该出现在每条记录里,AE 域的 AESLIFE、AESER、AEREL 这些严重不良事件标识变量有没有漏填,--DTC 和 RFSTDTC 算出来的 --DY 对不对,这些问题靠人眼逐行看 XPT 文件基本等于自虐。更麻烦的是,SDTMIG 3.2 对核心变量分了必需(Required)、期望(Expected)、许可(Permissible)三档,必需变量每条记录都不能为空,期望变量允许有空值但列必须存在,许可变量全空时申办方可以决定是否保留。这三档规则混在一起,手工核对一个域就要大半天,多域并行的时候根本扛不住。
我试过用纯脚本硬写校验逻辑,结果光是变量角色分类就写了几百行,标识符变量、主题变量、时间变量、修饰语变量、规则变量五类还没理清,修饰语下面又分分组修饰语、结果修饰语、同义词修饰语、记录修饰语、变量修饰语五个子类,写到后面自己都绕晕了。后来换了个思路,把 SDTMIG 3.2 的域模型定义整理成结构化配置,再用大模型 API 做变量级语义校验,一次配置就能跑通 DM、AE、LB 多个域的检查。这篇就把这套可复制的 config.toml 骨架和 TaoToken 统一 Key 配置完整交出来,你照着配就能在本地跑起来。
2. TaoToken 统一 Key 前置准备
2.1 为什么校验场景需要统一 Key
SDTM 校验不是跑一次就完事的活。DM 域校验完要跑 AE,AE 跑完要跑 LB,每个域可能还要反复调提示词、换模型对比结果。如果每个模型单独配 Key,光是管理密钥就够烦的。TaoToken 的做法是一个 Key 打通多个模型,你在 config.toml 里只维护一份凭证,切换模型只改模型名不改 Key。对临床数据程序员来说,这意味着校验脚本的配置层和模型层解耦了,今天用这个模型跑 DM,明天换那个模型跑 AE,配置文件不用动。
2.2 获取 Key 与可用入口
先到官网注册账号,地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,注册完进控制台创建 API Key。控制台入口在 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,Key 管理页面在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。API 基础地址统一用 https://taotoken.net/api ,注意这个地址后面不加任何 UTM 参数,直接写进配置就行。
注意:Key 创建后只显示一次,复制到本地配置文件后不要再提交到 Git 仓库。建议用环境变量注入,config.toml 里只写占位符。
2.3 模型选择建议
变量校验这种任务对模型的指令遵循能力要求比较高,因为 SDTMIG 3.2 的规则很细,模型得能准确理解“必需变量不能为空”和“期望变量列必须存在但值可空”的区别。实测下来,长上下文模型在处理多域变量清单时表现更稳,因为 DM 加 AE 加 LB 的变量定义拼起来轻松超过几千 token。你可以在模型对话页面先手动试几条校验指令,看看模型对核心变量三档分类的理解准不准,地址是 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。如果后面要长期跑批量校验或者接 Agent 自动修数据,可以看 Coding Plan 页面 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,按量或包月看你跑的频率定。
3. 可复制的 config.toml 骨架与域模型定义
3.1 config.toml 完整骨架
下面这份配置直接复制就能用,把 api_key 换成你自己的就行。base_url 固定写 https://taotoken.net/api ,不要加斜杠结尾。
# SDTMIG 3.2 变量校验配置 [llm] provider = "taotoken" base_url = "https://taotoken.net/api" api_key = "sk-你的Key写这里" model = "gpt-4o" # 可换成其他长上下文模型 max_tokens = 4096 temperature = 0.1 # 校验任务要低温度,减少发挥 [sdtm] version = "3.2" domains = ["DM", "AE", "LB"] strict_mode = true # 必需变量为空直接报错 [validation] check_required = true # 检查必需变量非空 check_expected = true # 检查期望变量列存在 check_permissible = false # 许可变量全空不报错 check_dy = true # 检查 --DY 计算 reference_date_var = "RFSTDTC" [output] format = "json" report_path = "./sdtm_validation_report.json"3.2 DM 域变量定义片段
把 SDTMIG 3.2 的域模型转成结构化定义,下面以 DM 域为例。DM 是特殊目的域,核心变量包括 STUDYID、USUBJID、DOMAIN、SUBJID、RFSTDTC、RFENDTC、SITEID、AGE、SEX、RACE 等。注意 AGE、SEX、RACE 属于记录修饰语,在 DM 域里是期望变量,列必须存在但允许个别记录为空。
{ "domain": "DM", "label": "Demographics", "class": "Special Purpose", "variables": [ {"name": "STUDYID", "role": "Identifier", "core": "Req", "type": "Char"}, {"name": "DOMAIN", "role": "Identifier", "core": "Req", "type": "Char"}, {"name": "USUBJID", "role": "Identifier", "core": "Req", "type": "Char"}, {"name": "SUBJID", "role": "Identifier", "core": "Req", "type": "Char"}, {"name": "RFSTDTC", "role": "Timing", "core": "Exp", "type": "Char"}, {"name": "RFENDTC", "role": "Timing", "core": "Exp", "type": "Char"}, {"name": "SITEID", "role": "Identifier", "core": "Exp", "type": "Char"}, {"name": "AGE", "role": "Record Qualifier", "core": "Exp", "type": "Num"}, {"name": "AGEU", "role": "Variable Qualifier", "core": "Exp", "type": "Char"}, {"name": "SEX", "role": "Record Qualifier", "core": "Exp", "type": "Char"}, {"name": "RACE", "role": "Record Qualifier", "core": "Exp", "type": "Char"}, {"name": "COUNTRY", "role": "Record Qualifier", "core": "Perm", "type": "Char"} ] }3.3 AE 域变量定义片段
AE 域属于事件类,主题变量是 AETERM,标识变量 STUDYID、USUBJID、DOMAIN、--SEQ 必须存在。AESER、AESLIFE、AEREL 这些是记录修饰语,用来描述严重不良事件的属性。注意 AESTDTC 和 AEENDTC 是时间变量,--DY 的计算要基于 RFSTDTC。
{ "domain": "AE", "label": "Adverse Events", "class": "Events", "variables": [ {"name": "STUDYID", "role": "Identifier", "core": "Req", "type": "Char"}, {"name": "DOMAIN", "role": "Identifier", "core": "Req", "type": "Char"}, {"name": "USUBJID", "role": "Identifier", "core": "Req", "type": "Char"}, {"name": "AESEQ", "role": "Identifier", "core": "Req", "type": "Num"}, {"name": "AETERM", "role": "Topic", "core": "Req", "type": "Char"}, {"name": "AEDECOD", "role": "Synonym Qualifier", "core": "Exp", "type": "Char"}, {"name": "AESER", "role": "Record Qualifier", "core": "Exp", "type": "Char"}, {"name": "AESLIFE", "role": "Record Qualifier", "core": "Perm", "type": "Char"}, {"name": "AEREL", "role": "Record Qualifier", "core": "Exp", "type": "Char"}, {"name": "AESTDTC", "role": "Timing", "core": "Exp", "type": "Char"}, {"name": "AEENDTC", "role": "Timing", "core": "Perm", "type": "Char"}, {"name": "AESTDY", "role": "Timing", "core": "Perm", "type": "Num"}, {"name": "AEENDY", "role": "Timing", "core": "Perm", "type": "Num"} ] }3.4 变量角色与核心分类对照
SDTMIG 3.2 的变量角色分五类,核心分三档,对照关系如下表。校验逻辑就是按这个表来的:标识符变量和主题变量通常是必需,时间变量和修饰语变量按域不同分期望或许可。
| 变量角色 | 说明 | 典型核心档 | 示例 |
|---|---|---|---|
| Identifier | 标识研究、受试者、域、记录序号 | Req | STUDYID, USUBJID, DOMAIN, --SEQ |
| Topic | 观测记录的主要目的 | Req | AETERM, --TESTCD |
| Timing | 描述观测时间 | Exp/Perm | --DTC, --STDY, --ENDY |
| Qualifier | 进一步描述结果或记录特征 | Exp/Perm | AGE, SEX, AESER, --ORRESU |
| Rule | 表达算法或可执行方法 | Exp | --DRVFL, --EVAL |
修饰语变量再细分五个子类:分组修饰语(--CAT、--SCAT)、结果修饰语(--ORRES、--STRESC、--STRESN)、同义词修饰语(--MODIFY、--DECOD)、记录修饰语(--REASND、AGE、SEX、RACE)、变量修饰语(--ORRESU、--ORNRHI、--ORNRLO)。校验时变量修饰语只能结合被修饰变量使用,单独出现没有意义。
4. 用 Cline 调用 API 完成变量级校验
4.1 Cline 配置接入
Cline 是 VS Code 里的编码助手插件,配好 API 后可以直接在编辑器里跑校验脚本。打开 Cline 设置,API Provider 选 OpenAI Compatible,Base URL 填 https://taotoken.net/api ,API Key 填你在控制台创建的那个,Model ID 填 config.toml 里写的模型名。配置文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有各客户端的接入示例。
4.2 校验提示词模板
把域定义和待校验数据一起塞给模型,提示词要明确告诉它按 SDTMIG 3.2 的核心变量规则来判。下面这个模板可以直接用:
VALIDATION_PROMPT = """ 你是 SDTMIG 3.2 合规校验专家。根据以下域模型定义,校验数据集变量是否符合规范。 域模型定义: {domain_definition} 待校验数据集列名与样本值: {dataset_sample} 校验规则: 1. 必需(Req)变量必须存在且所有记录非空,缺失则报 ERROR。 2. 期望(Exp)变量列必须存在,允许部分记录为空,列缺失报 ERROR,值空报 WARNING。 3. 许可(Perm)变量全空时不报错,有值但类型不符报 WARNING。 4. 检查 --DY 计算:--DY = (--DTC日期 - RFSTDTC日期) + 1,若 --DTC 早于 RFSTDTC 则不加 1。 5. 变量修饰语必须与其修饰的变量同时存在。 输出 JSON 格式:{{"domain": "...", "errors": [...], "warnings": [...]}} """4.3 调用脚本与结果解析
用 Python 调 API 的完整脚本如下,读 config.toml、拼提示词、发请求、解析 JSON 报告一条龙:
import tomllib import json import requests with open("config.toml", "rb") as f: cfg = tomllib.load(f) def validate_domain(domain_def, dataset_sample): prompt = VALIDATION_PROMPT.format( domain_definition=json.dumps(domain_def, ensure_ascii=False), dataset_sample=json.dumps(dataset_sample, ensure_ascii=False) ) resp = requests.post( f"{cfg['llm']['base_url']}/v1/chat/completions", headers={"Authorization": f"Bearer {cfg['llm']['api_key']}"}, json={ "model": cfg["llm"]["model"], "messages": [{"role": "user", "content": prompt}], "temperature": cfg["llm"]["temperature"], "max_tokens": cfg["llm"]["max_tokens"] }, timeout=120 ) resp.raise_for_status() content = resp.json()["choices"][0]["message"]["content"] return json.loads(content) # 示例:校验 DM 域 dm_def = json.load(open("./domains/dm.json")) dm_sample = {"USUBJID": ["S001", "S002"], "AGE": [45, None], "SEX": ["M", "F"]} result = validate_domain(dm_def, dm_sample) print(json.dumps(result, ensure_ascii=False, indent=2))4.4 多域批量校验
把 domains 列表里的域逐个跑一遍,结果汇总到一份报告:
report = {} for domain in cfg["sdtm"]["domains"]: ddef = json.load(open(f"./domains/{domain.lower()}.json")) dsample = load_dataset_sample(domain) # 你的数据读取函数 report[domain] = validate_domain(ddef, dsample) with open(cfg["output"]["report_path"], "w") as f: json.dump(report, f, ensure_ascii=False, indent=2)5. 验证请求与成功结果
5.1 单域校验请求示例
拿 DM 域跑一次,请求体里带上域定义和样本数据。注意样本数据不用全量,每个变量给两三条代表性记录就行,模型看的是列名和值模式,不是逐行核对。
curl -X POST https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer sk-你的Key" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o", "messages": [{"role": "user", "content": "校验DM域:USUBJID=[S001,S002], AGE=[45,null], SEX=[M,F], 域定义中AGE为Exp变量..."}], "temperature": 0.1 }'5.2 成功返回结果
模型返回的 JSON 报告里,errors 数组为空说明必需变量都合规,warnings 里会列出期望变量的空值情况。下面是一个典型成功返回:
{ "domain": "DM", "errors": [], "warnings": [ { "variable": "AGE", "level": "WARNING", "message": "AGE 为期望变量,第2条记录值为空,列存在符合规范" } ], "summary": "DM 域必需变量全部存在且非空,期望变量列完整,校验通过" }5.3 AE 域 --DY 计算校验
AE 域的 AESTDY 和 AEENDY 是许可变量,但如果填了值就得符合 --DY 计算公式。给模型一条 AESTDTC 早于 RFSTDTC 的记录,看它能不能正确判断不加 1:
{ "domain": "AE", "errors": [], "warnings": [ { "variable": "AESTDY", "level": "WARNING", "message": "AESTDTC=2023-01-05 早于 RFSTDTC=2023-01-10,AESTDY 应为 -5 而非 -4" } ] }6. 本篇常见错排查
6.1 401 报错:Key 没带上或格式不对
最常见的就是 Authorization 头写错。正确格式是Bearer sk-xxx,Bearer 和 Key 之间一个空格,Key 前面不要加引号。如果你用环境变量注入,确认 shell 里 export 了再跑脚本。另外检查 base_url 是不是写成了 https://taotoken.net/api/ 带斜杠,带斜杠会导致路径拼接出问题,去掉末尾斜杠。
6.2 模型返回非 JSON 导致解析失败
校验提示词里虽然要求输出 JSON,但模型偶尔会加解释性文字。解决办法是在解析前先做一次清洗,用正则把第一个{到最后一个}之间的内容抠出来再 json.loads。如果还是失败,把 temperature 再调低到 0,或者在提示词末尾加一句“只输出 JSON,不要任何其他文字”。
6.3 必需变量误判为空
有时候数据里必需变量看着有值,但模型报空。检查一下是不是把空字符串""当成了有效值。SDTM 里空字符串和 null 都算空,校验脚本里要先做归一化,把""转成 None 再传给模型。另外注意 XPT 文件读出来字符型变量的空格填充," "也要 strip 后再判空。
6.4 --DY 计算边界情况
RFSTDTC 本身为空的时候,所有 --DY 都没法算,这时候应该报 WARNING 而不是 ERROR,因为 RFSTDTC 在 DM 域是期望变量,允许为空。模型有时候会把这个判成 ERROR,你可以在提示词里补一句“RFSTDTC 为空时 --DY 相关检查跳过,仅报 WARNING”。
6.5 多域并行时 token 超限
DM 加 AE 加 LB 的域定义拼起来可能超过模型上下文窗口。解决办法是分域跑,不要一次把所有域塞进一个请求。config.toml 里的 domains 列表就是干这个的,脚本里循环逐个域调用,每个请求只带当前域的定义和数据。如果单个域的变量特别多,可以把变量定义精简成只保留 name、role、core 三个字段,type 和 label 校验时用不上。
6.6 Cline 里模型名填错
Cline 的 Model ID 必须和 TaoToken 支持的模型名完全一致,大小写敏感。填错了会报 model not found。不确定的话先去模型对话页面确认可用模型列表,地址是 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite ,复制模型名再填到 Cline 里。
整套配置跑通之后,DM、AE、LB 三个域的变量级校验大概两分钟出报告,比手工核对快了一个数量级。后面如果要接 CI 流水线,把校验脚本包成命令行工具,每次数据更新自动跑一遍就行。长期做编码和 Agent 自动化的,Coding Plan 那边有更省事的方案,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,按你的跑量选就行。