☰
基于OpenEvals的自动化评估-14:用静态代码分析给Agent生成的Python代码做体检
2026/9/28 4:20:14 网站建设 项目流程

1. Agent 写完代码,谁来签字放行

Coding Agent 现在几乎成了大模型落地最卷的一条赛道。你给它一句需求,它噼里啪啦吐出一段 Python,看起来结构完整、注释齐全,甚至还会贴心地加上类型注解。但问题是:这段代码真的能过吗?变量名写错了没有?返回值类型对得上注解吗?有没有引用一个根本不存在的函数?

我见过太多团队的做法是——把 Agent 生成的代码直接丢进沙箱跑一遍,能跑通就算过。这个思路没错,但漏掉了一整类问题:运行时不一定报错、但工程上已经烂掉的代码。比如一个函数声明返回int,实际在某条分支返回了字符串"Invalid Age",只要那条分支没被触发,测试就是绿的;再比如f"Hello {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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=console

拿到 Key 之后,把它写进环境变量,别硬编码在脚本里:

export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"

如果你打算用code_extraction_strategy="llm"让裁判模型帮忙捞代码,还需要指定模型名。模型列表可以在模型对话页确认:

模型对话:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=models

装依赖这一步别偷懒,Pyright 是 Node.js 写的,得单独装 CLI:

pip 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_blocks"
code_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 seq

"""

code_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 f"Hello {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, indent=2, ensure_ascii=False)) else: print(json.dumps(result, indent=2, ensure_ascii=False))

async def main() -> None: evaluator = create_async_pyright_evaluator( code_extraction_strategy="markdown_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(outputs=payload) 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变成false,comment里是 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,却在分支里返回了字符串,触发reportReturnType;greet_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_strategy="markdown_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 Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=coding-plan

接入文档里有完整的评估器 API 说明和更多代码提取策略的示例,遇到参数不确定的时候直接查这里最快:

接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=doc

最后说个实操经验:静态分析的误报率和你传给它的代码上下文强相关。Agent 生成的往往是代码片段而非完整模块,缺 import、缺类型定义都会导致误报。我的做法是在评估前先给代码补一个最小化的 stub 头,把常用的typing导入和占位类型声明加上,误报能降一大半。这个 stub 不用很精确,Pyright 的类型推导能力足够从上下文里补全大部分信息。

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

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

立即咨询