Scanpy 单细胞流程太散?用 TaoToken 给 Codex 换 Base URL 再调科研 Skill
2026/9/18 18:47:12 网站建设 项目流程

1. 报错现场还原:Codex 调 Scanpy 技能为什么总在 QC 步断掉

昨天下午三点,师弟把培养了三天的 PBMC 10X 数据丢进 Codex,让它照着科研技能库里已经装好的单细胞 Skill 跑一遍质控。二十分钟后终端弹出一行ValueError: cannot reshape array of size ...,往上翻还能看到anndata被某个依赖悄悄升到了 0.11,scanpy.pp.scrublet读不到预期的layers["counts"]。他以为是模型太笨,其实是三件事叠在一起:第一,Codex 默认走的是官方端点,Scanpy 这类要读几十万细胞矩阵的长上下文任务跑到一半就容易被截断;第二,技能文档里固化的分析流程没有和本地scanpy版本对齐;第三,多轮对话里 Agent 自己「重新发明」了一遍阈值,两次运行结果根本对不上。

这篇文章不聊宏观趋势,只按生信工程师的实际操作顺序走:先到 TaoToken( https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=scanpy_codex_intro )把 Key 拿下来,把 Base URL 改成https://taotoken.net/api,再让 Codex 老老实实按 Skill 文档串联 Scanpy 质控、去双细胞、Cellxgene Census 整合这几步。最后给出一套可对照的重跑指纹,用来验证 Agent 到底有没有「每次都跑同一条流水线」。

科研技能库这一波确实热,把隐性工作流打包成可安装技能是很好的方向。但工具链再好,落到本地机器上仍然是「模型端点 + 依赖版本 + 流程参数」三件套的安全问题。下面按顺序拆。

2. 十分钟准备:拿到 Key、锁定端点、确认版本

第一步不是装 Skill,而是先把供应商端点固定住。链路不稳,后面所有重跑对照都没有意义。

打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=scanpy_codex_prepare ,进入控制台创建一枚 API Key。本文所有示例统一用占位符YOUR_API_KEY,请不要把真实 Key 直接写进任何会进 Git 的文件。

Base URL 固定为:

https://taotoken.net/api

这个地址在 Codex 和 Claude Code 里写法不同,但值是一样的。建议先在模型对话页( https://taotoken.net/models/detail/chat?utm_source=taotoken_aicg_blog_end&utm_content=scanpy_codex_chat )用自然语言问一句「Scanpy 做 PBMC 质控,线粒体阈值一般取多少」,确认 Key 和端点通不通,再进入本地配置。

第二步是版本锁定。Scanpy 生态的坑几乎全部来自版本漂移,建议在项目根目录用uv建一个独立环境:

uv venv .venv-sc --python 3.13 source .venv-sc/bin/activate uv pip install \ "scanpy==1.11.0" \ "anndata==0.11.3" \ "scrublet==0.2.3" \ "harmonypy==0.0.10" \ "cellxgene-census==1.16.2" \ "leidenalg==0.10.2"

版本号写进requirements.lock,Agent 每次生成脚本前必须读这个文件。这一步比任何提示词技巧都有效——它直接消灭了「上次能跑这次不能跑」的大半原因。

第三步是 Skill 的安装范围。科研技能库动辄上百项技能,全量装进 Codex 只会让任务路由变得混乱,误调用概率明显上升。单细胞课题只装scanpycellxgenepydeseq2这三四个就够了。装完务必打开对应目录下的SKILL.md,看清楚它默认的 QC 阈值、是否强制去双细胞、有没有要求特定 layer 名称,这些细节决定了你后面要不要覆盖它。

3. Codex 侧配置:config.toml 写死 provider,别碰 ANTHROPIC_*

Codex 的供应商配置在~/.codex/config.toml。注意一个高频错误:网上很多教程把 Claude Code 的ANTHROPIC_*环境变量直接搬给 Codex,这完全是两套体系,Codex 不认。Codex 用的是model_provider+[model_providers.*]结构。

# ~/.codex/config.toml model = "gpt-5-codex" model_provider = "taotoken" model_reasoning_effort = "high" # 第三方兼容端点建议关闭响应存储,避免长任务被服务端状态影响 disable_response_storage = true [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" # 兼容 OpenAI Chat Completions 协议的端点用 chat; # 若你的端点确认支持 Responses API,可改为 responses wire_api = "chat" request_max_retries = 4 stream_max_retries = 6

Key 通过环境变量注入,不要写进配置文件:

# macOS / Linux export TAOTOKEN_API_KEY="YOUR_API_KEY" echo 'export TAOTOKEN_API_KEY="YOUR_API_KEY"' >> ~/.zshrc
# Windows PowerShell $env:TAOTOKEN_API_KEY = "YOUR_API_KEY" [Environment]::SetEnvironmentVariable("TAOTOKEN_API_KEY", "YOUR_API_KEY", "User")

配完做一次连通性验证,别急着丢 10X 数据:

codex exec --provider taotoken \ "用一句话说明 scanpy.pp.calculate_qc_metrics 里 qc_vars 参数的作用,不要执行代码"

能稳定返回就说明 Base URL 生效了。如果返回 401,先检查TAOTOKEN_API_KEY有没有真的进到当前 shell;返回 404,检查base_url是不是被误加成了/v1/v1这类重复后缀。这一步的排障思路在官网文档里也有对应说明( https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=scanpy_codex_codex_config )。

还有一个容易忽略的点:Codex 的工作目录权限。让 Agent 读写data/scripts/results/三个目录就行,别把整个家目录放进去。单细胞原始数据动辄几十 GB,Agent 一旦递归扫描会直接卡死。

4. Claude Code 与 CC Switch:settings.json + 环境变量 + provider 三件套

如果你同时用 Claude Code 做论文写作、用 Codex 跑生信流程,那就需要把两边的配置分开管理。Claude Code 用的是ANTHROPIC_*系列,和上面的 Codex 配置完全不通用。

方式一,项目级settings.json

{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "claude-sonnet-4-5", "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1" }, "permissions": { "allow": [ "Bash(python scripts/*.py)", "Read(./results/**)", "Write(./results/**)" ], "deny": [ "Read(./data/raw/**)", "Bash(rm -rf *)" ] } }

方式二,Shell 环境变量,适合临时切换:

export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_AUTH_TOKEN="YOUR_API_KEY" export ANTHROPIC_MODEL="claude-sonnet-4-5"

方式三,CC Switch 里新增一个 provider。CC Switch 的三件套就是ANTHROPIC_BASE_URLANTHROPIC_AUTH_TOKENANTHROPIC_MODEL这三项,填完保存,切换配置时它会改写 Claude Code 的实际生效环境。生信工程师常见的工作流是:跑数据切到 Codex + TaoToken,写论文切到 Claude Code + TaoToken,两边 Key 分开管,额度互不干扰。

这里再强调一次边界:Codex 用config.toml,Claude Code 用settings.jsonANTHROPIC_*,两者的 Key 变量名也不一样。混用是新手最常见的 401 来源。

如果团队需要统一额度口径,可以直接看 Coding Plan( https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=scanpy_codex_plan ),把多个工具归到同一个计费入口,省掉逐台机器对账的麻烦。

5. 让 Agent 跑出可复现的 Scanpy QC:阈值必须外置

装好 Skill 之后,Agent 通常会给出一版「看起来对」的 QC 脚本。问题在于它是每次都重新生成,阈值随机漂移。正确做法是把阈值抽成一份独立配置,脚本只读配置,Agent 只负责按配置执行。

先写配置文件:

# config/qc.yaml qc: min_genes: 200 max_genes: 6000 max_pct_mt: 15 max_pct_hb: 5 min_cells_per_gene: 3 n_top_genes: 2000 doublet: enabled: true n_prin_comps: 30 batch_key: sample embedding: n_comps: 50 n_neighbors: 15 n_pcs: 30 leiden_resolution: 0.8 random_state: 0

再让 Agent 严格按这份配置执行。下面是可直接运行的脚本,建议命名为scripts/qc_scanpy.py,由本地终端执行,不要让 Agent 直接连数据库或远端存储:

# scripts/qc_scanpy.py import os import json import yaml import scanpy as sc with open("config/qc.yaml", "r", encoding="utf-8") as fh: cfg = yaml.safe_load(fh) sc.settings.verbosity = 2 sc.settings.figdir = "results/figs" os.makedirs(sc.settings.figdir, exist_ok=True) adata = sc.read_10x_mtx( "data/filtered_feature_bc_matrix", var_names="gene_symbols", cache=True, ) adata.var_names_make_unique() adata.layers["counts"] = adata.X.copy() # 基因层面过滤 sc.pp.filter_genes(adata, min_cells=cfg["qc"]["min_cells_per_gene"]) # 标记三类高风险基因群 adata.var["mt"] = adata.var_names.str.startswith("MT-") adata.var["ribo"] = adata.var_names.str.startswith(("RPS", "RPL")) adata.var["hb"] = adata.var_names.str.contains(r"^HB[ABDEGQZ]\d", regex=True) sc.pp.calculate_qc_metrics( adata, qc_vars=["mt", "ribo", "hb"], percent_top=None, log1p=False, inplace=True, ) # 细胞层面过滤:先用固定阈值挡掉明显破损细胞 sc.pp.filter_cells(adata, min_genes=cfg["qc"]["min_genes"]) adata = adata[adata.obs["n_genes_by_counts"] < cfg["qc"]["max_genes"], :].copy() adata = adata[adata.obs["pct_counts_mt"] < cfg["qc"]["max_pct_mt"], :].copy() adata = adata[adata.obs["pct_counts_hb"] < cfg["qc"]["max_pct_hb"], :].copy() # 去双细胞:按样本分别处理,避免跨样本嵌合 if cfg["doublet"]["enabled"]: sc.pp.scrublet( adata, batch_key=cfg["doublet"]["batch_key"], n_prin_comps=cfg["doublet"]["n_prin_comps"], random_state=cfg["embedding"]["random_state"], ) adata = adata[~adata.obs["predicted_doublet"], :].copy() # 归一化与对数化 sc.pp.normalize_total(adata, target_sum=1e4) sc.pp.log1p(adata) # 高变基因 sc.pp.highly_variable_genes( adata, n_top_genes=cfg["qc"]["n_top_genes"], batch_key=cfg["doublet"]["batch_key"], flavor="seurat", ) adata.raw = adata adata = adata[:, adata.var["highly_variable"]].copy() # 降维聚类 sc.pp.scale(adata, max_value=10) sc.tl.pca( adata, svd_solver="arpack", n_comps=cfg["embedding"]["n_comps"], random_state=cfg["embedding"]["random_state"], ) sc.pp.neighbors( adata, n_neighbors=cfg["embedding"]["n_neighbors"], n_pcs=cfg["embedding"]["n_pcs"], random_state=cfg["embedding"]["random_state"], ) sc.tl.umap(adata, random_state=cfg["embedding"]["random_state"]) sc.tl.leiden( adata, resolution=cfg["embedding"]["leiden_resolution"], key_added="leiden", random_state=cfg["embedding"]["random_state"], ) adata.write_h5ad("results/qc_processed.h5ad") print(json.dumps({ "n_obs": int(adata.n_obs), "n_vars": int(adata.n_vars), "clusters": int(adata.obs["leiden"].nunique()), }, ensure_ascii=False))

这份脚本有两个关键设计。第一,所有阈值来自qc.yaml,Agent 想改就得改配置,改动会进 Git,可审计。第二,random_state全部显式传入,PCA、neighbors、UMAP、leiden 都是随机算法,不固定种子重跑必然漂移。

对应地,给 Codex 的指令也要写死约束,而不是让它自由发挥:

读取 config/qc.yaml,按 scripts/qc_scanpy.py 的现有流程执行, 不要新增或修改任何阈值参数; 不要联网下载参考数据; 执行完成后输出 n_obs、n_vars、leiden cluster 数三项, 并把完整 stdout 写入 results/logs/qc_run.log。

6. Cellxgene Census 整合参数:固定版本、固定过滤器

单细胞分析里最容易「每次结果都不一样」的环节是参考数据集整合。Cellxgene Census 的在线版本会更新,obs_value_filter写得太宽会拉进来几十万细胞,写得太窄又拿不到足够标签。所以版本号和过滤器必须一起固化。

# scripts/integrate_census.py import cellxgene_census import scanpy as sc CENSUS_VERSION = "2024-07-01" # 固定版本,保证可复现 TISSUE = "lung" MIN_NNZ = 50 with cellxgene_census.open_soma(census_version=CENSUS_VERSION) as census: ref = cellxgene_census.get_anndata( census, organism="Homo sapiens", obs_value_filter=( f"tissue_general == '{TISSUE}' " "and is_primary_data == True " "and disease == 'normal' " "and assay in ['10x 3\\' v3', '10x 5\\' v2']" ), var_value_filter=f"feature_type == 'gene' and nnz > {MIN_NNZ}", obs_column_names=["cell_type", "tissue_general", "assay", "dataset_id"], column_names={"obs": ["soma_joinid"]}, ) # 与本项目数据共享基因集 query = sc.read_h5ad("results/qc_processed.h5ad") shared = query.var_names.intersection(ref.var_names) query = query[:, shared].copy() ref = ref[:, shared].copy() # Harmony 批次校正:参数透传给 harmonypy sc.external.pp.harmony_integrate( query, key="sample", basis="X_pca", adjusted_basis="X_pca_harmony", max_iter_harmony=20, theta=2.0, random_state=0, ) query.write_h5ad("results/qc_harmony.h5ad") ref.write_h5ad("results/census_ref.h5ad") print(f"shared genes: {len(shared)}, query cells: {query.n_obs}, ref cells: {ref.n_obs}")

几个参数值得单独说清楚。

census_version不要省略。不写版本,API 会返回最新版,半年后同一份代码跑出来的参考集可能完全不同。is_primary_data == True用来剔除 Census 里重复收录的细胞,不加这个条件,某些数据集会被多重计数,整合时产生假的批次结构。nnz > 50控制稀疏基因,肺组织全量基因进去,内存会直接爆掉。assay白名单避免把 Smart-seq 和 10x 混在一起做 Harmony,这两类技术的基因检出率差异过大,混整合出来的 UMAP 看不出真实生物学结构。

Harmony 的theta控制校正强度,默认值偏保守;max_iter_harmony=20是收敛上限,如果你看到日志里 warning 说未收敛,可以提到 30,但不要无脑拉高,过度校正会把真实亚群抹平。random_state=0同样是复现的前提。

这三份产出——qc_processed.h5adqc_harmony.h5adcensus_ref.h5ad——就是本节点要求的可复现产物。它们全部由本地终端执行生成,Agent 只负责写脚本和读日志。

7. 任务重跑对照:同一 Prompt 两次执行差在哪

配好 Base URL 之后,一定要做一次重跑对照,否则你无法判断稳定性来自配置还是运气。方法是同一个 Prompt 连续执行三次,每次清空results/,只保留config/,然后比对指纹。

# scripts/fingerprint.py import json import scanpy as sc def fingerprint(path: str) -> dict: ad = sc.read_h5ad(path) return { "n_obs": int(ad.n_obs), "n_vars": int(ad.n_vars), "pct_mt_median": round(float(ad.obs["pct_counts_mt"].median()), 3), "doublet_rate": round(float(ad.obs["predicted_doublet"].mean()), 4), "hvg_count": int(ad.var["highly_variable"].sum()), "leiden_clusters": int(ad.obs["leiden"].nunique()), "umap_hash": str(abs(hash(tuple(ad.obsm["X_umap"][:50].round(4).flatten())))), } print(json.dumps(fingerprint("results/qc_processed.h5ad"), indent=2, ensure_ascii=False))

跑完之后按下面这张表核对:

对照项第一次第二次第三次判定标准
过滤后细胞数 n_obs三次完全一致
保留基因数 n_vars三次完全一致
pct_counts_mt 中位数波动 < 0.1
doublet_rate三次一致
HVG 数量恒等于 2000
leiden cluster 数允许 ±1
UMAP 前 50 点哈希建议一致

出现偏差时按下面的顺序排查,不要一上来就怀疑模型:

  • n_obs 不一致,先查qc.yaml是不是被 Agent 改过,对比git diff config/
  • doublet_rate 不一致,检查scrubletrandom_state有没有真的传进去,以及batch_key指定的样本列是否存在空值。
  • HVG 数量不是 2000,说明flavorbatch_key被改动,flavor="seurat_v3"需要原始 counts,而你此时adata.X已经是 log 后的值,会让结果偏掉。
  • cluster 数大幅波动,通常是sc.pp.neighborsn_pcs被改,或者 PCA 的random_state缺失。

如果三次跑下来前六项完全一致,说明 Base URL 切换 + 配置外置 + 随机种子固定这套组合是有效的。这时候再去扩展流程,比如接 PyDESeq2 做差异表达、接 Reactome/KEGG 做通路富集,风险就小得多。

另外,长任务建议开日志落盘。Codex 的exec模式下把 stdout 重定向到results/logs/,Agent 中断时你至少还能从日志里知道卡在哪一步,而不是重新跑一遍。

8. 避坑清单与下一步

最后收几条实战经验,都是踩过的。

第一,Codex 和 Claude Code 的配置不要互相套。Codex 走config.tomlmodel_provider,Claude Code 走settings.jsonANTHROPIC_*。把ANTHROPIC_AUTH_TOKEN塞给 Codex,只会得到一串 401。

第二,Key 不要进 Git。用环境变量或本地密钥管理,config.toml里只写env_key的变量名。团队协作时把.env加进.gitignore

第三,科研技能不要全量安装。装得越多,Agent 的路由判断负担越重,误调用越频繁。单细胞课题就装生信相关的那几个,每次新增技能前读一遍SKILL.md,确认它的默认参数和你的qc.yaml是否冲突。

第四,敏感数据不出本地。未发表的测序数据、患者隐私信息、企业合作项目,不要交给任何云端 Agent 处理。本文所有脚本都设计成在本地终端执行,Agent 的职责限于生成脚本、解释报错、整理报告。

第五,阈值要进版本控制。qc.yaml是这套流程里最重要的文件,它的每次改动都应该有 commit message。这比任何 Prompt 工程技巧都更能保证半年后别人能复现你的分析。

第六,版本锁定文件跟着项目走。requirements.lockqc.yaml一起提交,scanpyanndataharmonypycellxgene-census四个包的版本尤其关键。

把上面这套跑通之后,链路是清晰的:先在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=scanpy_codex_final 拿到 Key,Base URL 固定为https://taotoken.net/api,Codex 走config.toml,Claude Code 走settings.json,两者互不干扰。接下来可以考虑把差异表达和通路富集也接进同一套流水线,让 Skill 负责流程编排,你负责科学判断。

几个可以直接进入的入口:先在模型对话页试一发提示词( https://taotoken.net/models/detail/chat?utm_source=taotoken_aicg_blog_end&utm_content=scanpy_codex_chat_final ),确认端点通畅;然后看 Coding Plan( https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=scanpy_codex_plan_final )决定额度口径;接着到控制台创建正式 Key( https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=scanpy_codex_keys_final );如果你主要用 Claude Code 跑分析,配置细节对照 Claude Code 文档( https://taotoken.net/doc/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_content=scanpy_codex_doc_final )一步步改就行。

把环境配置、依赖排错、脚本重跑这些机械劳动压下去,剩下的时间才真正属于科学问题本身。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询