
1. Agent 写完代码谁来签字放行Coding Agent 现在几乎成了大模型落地最卷的一条赛道。你给它一句需求它噼里啪啦吐出一段 Python看起来结构完整、注释齐全甚至还会贴心地加上类型注解。但问题是这段代码真的能过吗变量名写错了没有返回值类型对得上注解吗有没有引用一个根本不存在的函数我见过太多团队的做法是——把 Agent 生成的代码直接丢进沙箱跑一遍能跑通就算过。这个思路没错但漏掉了一整类问题运行时不一定报错、但工程上已经烂掉的代码。比如一个函数声明返回int实际在某条分支返回了字符串Invalid Age只要那条分支没被触发测试就是绿的再比如fHello {user_name}里变量名拼错了如果这个函数压根没被调用你也发现不了。这就是静态代码分析要补的位。它不执行代码只读代码靠类型推导和控制流分析把这类潜伏 bug提前揪出来。放到 OpenEvals 的评估骨架里它就是一个独立的评估维度可执行性评估回答能不能跑静态分析评估回答写得对不对。这篇要交付的东西很具体一个能直接复制的 OpenEvals 评估脚本骨架、Pyright 与 Mypy 两套静态分析规则配置、以及一次完整的验证动作——从 Agent 输出文本到问题清单全程本地跑通。适合正在搭 Agent 评测流水线、或者想给代码生成质量加一道闸的工程师。前置条件只有两个本地有 Python 3.10 环境以及一个能调模型的 API Key。2. 前置准备把评估器和模型通道接上OpenEvals 的代码评估器本身不绑定任何模型厂商它只负责提取代码 → 落盘 → 调静态分析工具 → 解析报告这条链路。但如果你想让它在代码提取阶段用 LLM 兜底比如 Agent 输出格式很乱代码和解释混在一起就需要一个可用的模型通道。我这边统一走 TaoToken 的接口原因是它同时兼容 OpenAI 风格的 chat completions 和 Anthropic 风格的消息接口切换模型时不用改代码结构。先去控制台拿一个 API Key控制台入口https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentconsole拿到 Key 之后把它写进环境变量别硬编码在脚本里export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api如果你打算用code_extraction_strategyllm让裁判模型帮忙捞代码还需要指定模型名。模型列表可以在模型对话页确认模型对话https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodels装依赖这一步别偷懒Pyright 是 Node.js 写的得单独装 CLIpip install openevals langchain-openai npm install -g pyright pip install mypy装完验证一下两个工具都在 PATH 里pyright --version mypy --version如果pyright提示 command not found多半是 npm 全局 bin 目录没进 PATH用npm config get prefix看一下路径手动加进去即可。这一步踩过坑的人不少因为 Pyright 的 Python 包和 Node CLI 是两回事pip install pyright装的那个不一定能直接当命令行用。3. 可复制配置Pyright 评估器骨架OpenEvals 提供了两个工厂函数同步用create_pyright_evaluator异步用create_async_pyright_evaluator。参数定义完全一致核心就四个参数作用推荐值pyright_cli_args透传给 pyright 命令行的额外参数[--pythonversion, 3.11]code_extraction_strategy从原始文本提取代码的策略markdown_code_blockscode_extractor自定义提取函数一般不用client/model仅当策略为llm时需要二选一code_extraction_strategy三个选项的差别值得说清楚。none是原样落盘适合你的 Agent 被严格约束成只输出纯代码markdown_code_blocks会扫描python包裹的块这是最常用的llm则是再调一个模型当代码捞取工通过注册ExtractCode和NoCode两个工具声明来完成——注意这俩工具只有声明没有实现评估器只关心模型返回的 tool call 里code参数的值。下面是一个可以直接跑的异步骨架我把两段测试代码都放进去了import asyncio import json from typing import cast from openevals.code.pyright import create_async_pyright_evaluator from openevals.types import EvaluatorResult code_ok 这是一个计算斐波那契数列的函数 python def fibonacci(n: int) - list[int]: if n 0: return [] if n 1: return [0] seq: list[int] [0, 1] while len(seq) n: seq.append(seq[-1] seq[-2]) return seqcode_bad 这里是一个处理用户数据的函数def process_user_age(age_str: str) - int: if not age_str.isdigit(): return Invalid Age return int(age_str) def greet_user(name: str, age: int) - str: return fHello {user_name}, you are {age} years old.def pretty_print(result: EvaluatorResult) - None: if result[comment] is not None: formatted { key: result[key], score: result[score], comment: json.loads(result[comment]), metadata: result[metadata], } print(json.dumps(formatted, indent2, ensure_asciiFalse)) else: print(json.dumps(result, indent2, ensure_asciiFalse))async def main() - None: evaluator create_async_pyright_evaluator( code_extraction_strategymarkdown_code_blocks, pyright_cli_args[--pythonversion, 3.11], ) for name, payload in [(code_ok, code_ok), (code_bad, code_bad)]: print(f {name} ) result await evaluator(outputspayload) pretty_print(cast(EvaluatorResult, result))ifname main: asyncio.run(main())这里有个细节评估器内部已经固定写死了 --outputjson 和 --level error所以你传 pyright_cli_args 时不用再重复这两个。另外它解析报告时会主动忽略 reportMissingImports也就是缺第三方库这类错误不算失败——这个设计很合理因为 Agent 生成的代码片段本来就不一定带完整依赖。 ## 4. 验证请求跑一次看问题清单 把上面的脚本存成 eval_pyright.py直接执行 bash python eval_pyright.py第一段代码应该干净通过{ key: pyright_succeeded, score: true, comment: [], metadata: null }第二段代码会被抓出两个问题score变成falsecomment里是 Pyright 的原始 JSON 报告{ key: pyright_succeeded, score: false, comment: [ { severity: error, message: Type \Literal[Invalid Age]\ is not assignable to return type \int\, range: {start: {line: 2, character: 15}, end: {line: 2, character: 28}}, rule: reportReturnType }, { severity: error, message: \user_name\ is not defined, range: {start: {line: 8, character: 20}, end: {line: 8, character: 29}}, rule: reportUndefinedVariable } ] }两个问题分别是process_user_age声明返回int却在分支里返回了字符串触发reportReturnTypegreet_user里用了未定义的user_name形参明明是name触发reportUndefinedVariable。这两个都是典型的能跑通但会炸的坑——第一个只在非数字输入时炸第二个只在函数被调用时炸。如果你想把 Mypy 也接进来做双重校验工厂函数换成create_async_mypy_evaluator即可参数结构一模一样只是默认的mypy_cli_args更严格from openevals.code.mypy import create_async_mypy_evaluator mypy_evaluator create_async_mypy_evaluator( mypy_cli_args[ --no-incremental, --disallow-untyped-calls, --disallow-incomplete-defs, --ignore-missing-imports, ], code_extraction_strategymarkdown_code_blocks, )--disallow-untyped-calls和--disallow-incomplete-defs这两个参数会把没写类型注解也当成错误逼着 Agent 输出完全符合 PEP 标准的代码。代价是误报会变多适合对代码规范要求极高的场景。5. 本篇常见错排查Pyright 报 command not found。前面提过pip install pyright和npm install -g pyright是两套东西。评估器底层调的是命令行所以必须保证 Node 版的 pyright 在 PATH 里。用which pyright确认没有就检查 npm 全局前缀。score 一直是 false但 comment 里全是 reportMissingImports。正常情况下评估器会忽略这类错误。如果你看到它们出现在 comment 里说明你手动传了覆盖性的pyright_cli_args把默认的忽略逻辑冲掉了。检查一下有没有传--level之类的参数。代码提取出来是空的。最常见的原因是 Agent 输出用了py而不是python或者用了四个反引号。markdown_code_blocks策略对语言标记比较敏感。这种情况要么在 Agent 侧统一输出格式要么改用llm策略让裁判模型兜底。用 llm 策略时报 client/model 未设置。这两个参数必须至少给一个。给model的话评估器内部会用init_model创建BaseChatModel给client的话就直接用你传进去的实例。两个都不给会直接抛异常。异步评估器在 Jupyter 里报 event loop 错误。asyncio.run()不能在已有事件循环里嵌套调用。Jupyter 环境改用await evaluator(...)直接跑或者用nest_asyncio打补丁。Mypy 报 duplicate module named xxx。这是 Mypy 的缓存问题加--no-incremental就能规避默认参数里已经带了。如果你自己覆盖了mypy_cli_args记得把这个参数保留。6. 把静态分析接进你的评估流水线单跑一个评估器只是起点真正有价值的是把它挂到 OpenEvals 的评估集里和可执行性评估、LLM-as-judge 一起组成多维打分。我的建议是分两层Pyright 作为快速闸门跑在高并发流水线上因为它底层是 Node.js、输出原生 JSON吞吐很稳Mypy 作为严格闸门只在最终验收阶段跑用它的严格参数卡 PEP 规范。如果你要长期跑这套评估、或者把它接进 CI 和 Agent 的自我修正循环可以考虑用 Coding Plan 来管理模型调用配额避免评估任务和线上业务抢同一个 Key 的额度Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding-plan接入文档里有完整的评估器 API 说明和更多代码提取策略的示例遇到参数不确定的时候直接查这里最快接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc最后说个实操经验静态分析的误报率和你传给它的代码上下文强相关。Agent 生成的往往是代码片段而非完整模块缺 import、缺类型定义都会导致误报。我的做法是在评估前先给代码补一个最小化的 stub 头把常用的typing导入和占位类型声明加上误报能降一大半。这个 stub 不用很精确Pyright 的类型推导能力足够从上下文里补全大部分信息。