[基于OpenEvals的自动化评估-14]以静态代码分析方式评估Agent生成的代码

[基于OpenEvals的自动化评估-14]以静态代码分析方式评估Agent生成的代码

虽然我个人对Coding Agent一直持保留的态度,但不可否认的是这已经成为Agent应用最大的一个分支,所以如何有效地评估Agent生成的代码就显得尤为重要。针对生成的代码的评估可以验证代码文本的合法性,也可以验证其可执行性;可以利用没有隔离的代码揭示和执行环境,也可以借助Sandbox提供更加安全的隔离保障;评估代码可以在本地执行,也可以上传到云平台执行。本章介绍几种在本地进行的针对Python的代码评估器。

1. 基于Pyright的Python代码评估器

Pyright是由微软开发的一款静态类型检查工具,专为Python语言设计。它也是目前非常流行的VS Code插件Pylance的底层核心引擎。核心优势与特点包括:

  • 极致的速度:这是Pyright最显著的标签。它完全使用TypeScript编写,并在Node.js环境下运行。由于其创新的架构设计,它的检查速度通常比用Python编写的mypy快出3到5倍,在大型项目(数百万行代码)中优势尤为明显;
  • 零配置开箱即用:它不需要繁琐的配置。只要你的代码中包含了Python 3的类型注解,Pyright就能直接进行精准的推导和检查;
  • 高精度的类型推导:即使你没有给变量显式写出类型,Pyright也能根据上下文、函数的返回值以及第三方库的定义,非常聪明地推断出变量的真实类型;
  • 对最新Python特性的顶级支持:作为微软官方维护的项目,它对Python最新版本以及高级类型特性的支持速度极快。

1.1 代码评估器的创建

我们可以调用如下这两个工厂函数来创建基于基于Pyright的Python代码评估器。具体来说,create_pyright_evaluator函数用来创建作为同步评估器的SimpleEvaluator对象,而create_async_pyright_evaluator用来创建作为异步评估器的SimpleAsyncEvaluator对象。评估器并不要求提供存储的Python代码文本实施评估,因为它具有从给出文本提取代码的能力。

defcreate_pyright_evaluator(*,pyright_cli_args:list[str]=[],code_extraction_strategy:Literal["none","llm","markdown_code_blocks"]="none",code_extractor:Optional[Callable[[Any],str]]=None,client:Optional[BaseChatModel]=None,model:Optional[str]=None,)->SimpleEvaluatordefcreate_async_pyright_evaluator(*,pyright_cli_args:list[str]=[],code_extraction_strategy:Literal["none","llm","markdown_code_blocks"]="none",code_extractor:Optional[Callable[[Any],Union[str,Awaitable[str]]]]=None,client:Optional[BaseChatModel]=None,model:Optional[str]=None,)->SimpleAsyncEvaluator

两个工厂函数的参数说明如下:

  • pyright_cli_args:传递给底层pyright命令行工具的额外参数。默认情况下,脚本已经固定写死了--outputjson--level error。你可以通过这个参数传递其他Pyright支持的参数,例如:
    • [“–pythonversion”, “3.11”]:指定大模型生成的代码应该以Python 3.11的语法标准进行类型检查。
    • [“–skipunannotated”]:跳过对未编写类型注解的函数检查。
  • code_extraction_strategy:指定从LLM的原始文本回复中提取纯Python代码的策略,具有如下三个选项:
    • none:不做任何处理。直接将LLM返回的所有文本全部塞进.py文件。适用于你的大模型被严格限制了只能输出纯代码、不能带有任何自然语言解释的场景;
    • llm:使用大模型提取。如果被评估的模型输出非常混乱(例如把代码和大量文字混杂在一起且不用Markdown标签),评估器会调用另一个独立的大模型(裁判模型)来帮你把纯代码捞出来;
    • markdown_code_blocks: 提取Markdown代码块。评估器会自动扫描文本,只把被python ...包裹的代码提取出来进行测试。这是最常用的推荐设置。
  • code_extractor:自定义代码提取函数。如果上述三种自带的code_extraction_strategy策略都不满足你的需求,你可以自己写一个Python函数传给它;
  • client:如果code_extraction_strategy选择llm,则可以使用这个参数提供一个BaseChatModel组件调用LLM;
  • model: 如果code_extraction_strategy选择llm,则可以使用这个参数提供一个标准的模型名称。

1.2 评估的流程

该评估器的核心逻辑可以概括为以下四个步骤:

  • 代码提取:从LLM的原始输出中,提取出纯Python代码(支持直接读取、Markdown块提取或通过LLM再次提取);
  • 临时落盘:将提取出的代码写入系统的临时文件(.py);
  • 静态分析:在后台调用系统命令pyright对该临时文件进行扫描,并输出JSON 格式的错误报告;
  • 解析打分:解析报告,忽略缺失第三方库导入(reportMissingImports)的错误。若无其他错误则判定为通过(score = True),否则返回具体的错误列表(score = False)。

如果将code_extraction_strategy参数设置为llmclientmodel必须有一个被设置。如果设置的是model,则使用init_model函数来创建对应的BaseChatModel对象。代码的提取是通过注册如下所示的两个工具来完成的。具体来说,具体注册的是不具有实现的工具声明。如果成功提取到评估代码,会在AIMessage中返回针对ExtractCode工具的调用,并将代码片段作为code参数的值,评估器只需要提取此参数即可。与之类似,如果没有发现代码,则会调用另一个NoCode工具。

classExtractCode(TypedDict):"""Tool to call if there is code to extract. Omit installation instructions and shell commands."""code:strclassNoCode(TypedDict):"""Tool to call to indicate no code was found."""no_code:bool

代码提取会使用如下的系统提示词和用户提示词:

You are an expert software auditor. <Instructions> Your job is to extract code from a given text. - If there is code - extract it into a single script by calling the provided "ExtractCode" tool. - If there is no code to extract - call "NoCode". If you extract code, your response will be passed DIRECTLY into a code execution sandbox for further testing, so make sure to extract all code **without modifications**, even if it contains errors, since any modifications will ruin the integrity of the testing process. Omit installation instructions and shell commands from any code you extract. </Instructions>
Extract code from the following: <text> {outputs} </text>

1.3 实例演示

在如下的这段演示程序中,我们利用create_async_pyright_evaluator函数创建了一个SimpleAsyncEvaluator对象,我们将code_extraction_strategy参数设置成markdown_code_blocks。然后利用它了评估段包含Python代码的Markdown文本,并将执行结果以JSON形式输出。由于评估器会将检测到的错误以JSON字符串的形式存储在comment字段中,所以我们定义了辅助函数pretty_printcomment字段进行了格式化。

fromopenevals.code.pyrightimportcreate_async_pyright_evaluatorfromopenevals.typesimportEvaluatorResultfromtypingimportcastimportjson,asyncio code1=""" 这是一个计算斐波那契数列的Python函数: ```python def fibonacci(n: int) -> list[int]: if n <= 0: return [] if n == 1: return [0] sequence: list[int] = [0, 1] while len(sequence) < n: sequence.append(sequence[-1] + sequence[-2]) return sequence ``` """code2=""" 这里是一个处理用户数据的函数: ```python def process_user_age(age_str: str) -> int: if not age_str.isdigit(): return "Invalid Age" age = int(age_str) return age def greet_user(name: str, age: int) -> str: return f"Hello {user_name}, you are {age} years old." ``` """defpretty_print(result:EvaluatorResult):ifresult["comment"]!=None:formatted:dict={"key":result["key"],"score":result["score"],"comment":json.loads(result["comment"]),"metadata":result["metadata"],"source_run_id":result.get("source_run_id")}print(json.dumps(formatted,indent=2,ensure_ascii=False))returnprint(json.dumps(result,indent=2,ensure_ascii=False))asyncdefmain():evaluator=create_async_pyright_evaluator(code_extraction_strategy="markdown_code_blocks")result=awaitevaluator(outputs=code1)pretty_print(cast(EvaluatorResult,result))result=awaitevaluator(outputs=code2)pretty_print(cast(EvaluatorResult,result))asyncio.run(main())

输出:

{"key":"pyright_succeeded","score":true,"comment":[],"metadata":null,"source_run_id":null}
{"key":"pyright_succeeded","score":false,"comment":[{"severity":"error","message":"Type \"Literal['Invalid Age']\" is not assignable to return type \"int\"\n \"Literal['Invalid Age']\" is not assignable to \"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"}],"metadata":null,"source_run_id":null}

从输出可以看出,第一段代码并没有问题,第二段代码的如下两个问题被成功检测到:

  • process_user_age函数返回值与注解类型不一致;
  • greet_user函数使用了未定义的变量user_name

2. 基于Mypy的Python代码评估器

MyPy是Python社区中最官方、最老牌且使用最广泛的静态类型检查工具。它由Python之父Guido van Rossum深度参与开发并极力推崇,直接促成了Python 3.5引入官方的typing模块。基于Mypy的代码评估器与上面介绍的基于Pyright的评估器除了底层使用的静态代码分析引擎不一致,在设计和API的使用上基本是一样的。如下是用来创建同步和异步评估器的两个工厂函数create_mypy_evaluatorcreate_async_mypy_evaluator,它们具有与create_pyright_evaluator/create_async_pyright_evaluator函数完全一致的参数定义。

defcreate_mypy_evaluator(*,mypy_cli_args:list[str]=["--no-incremental","--disallow-untyped-calls","--disallow-incomplete-defs","--ignore-missing-imports",],code_extraction_strategy:Literal["none","llm","markdown_code_blocks"]="none",code_extractor:Optional[Callable[[Any],str]]=None,client:Optional[BaseChatModel]=None,model:Optional[str]=None,)->SimpleEvaluatordefcreate_async_mypy_evaluator(*,mypy_cli_args:list[str]=["--no-incremental","--disallow-untyped-calls","--disallow-incomplete-defs","--ignore-missing-imports",],code_extraction_strategy:Literal["none","llm","markdown_code_blocks"]="none",code_extractor:Optional[Callable[[Any],Union[str,Awaitable[str]]]]=None,client:Optional[BaseChatModel]=None,model:Optional[str]=None,)->SimpleAsyncEvaluator

3. 如何选择

其实两种评估差不了太多,如果一定有所选择的化,可以根据如下的策略来选:

  • 如果你追求极速与稳定性:建议使用前面的Pyright评估器,它的Node.js底层和原生JSON 输出非常适合高并发的AI自动化评测流水线;
  • 如果你追求Python官方的严苛标准:使用当前的Mypy评估器,并保留其默认的严格参数,可以逼迫大模型生成极其规范、完全符合PEP标准的现代 Python 代码;
  • 更高级的玩法(双剑合璧):在LangSmith评测集里同时注册这两个评估器。如果一个LLM生成的代码能同时通过Pyright和Mypy的双重评估,说明这段代码在任何工程环境下的类型安全度都已经达到了顶峰。