146 lines
7.4 KiB
Markdown
146 lines
7.4 KiB
Markdown
# Step 2b:实现 Runnable
|
||
|
||
> 关于完整的 `Runnable` 协议和 `wrap()` API,请参阅 `wrap-api.md`。
|
||
|
||
**目标**:编写一个 Runnable 类,让评估框架能够像真实用户一样调用应用程序。
|
||
|
||
---
|
||
|
||
## 核心思路
|
||
|
||
Runnable 是 `pixie test` 和 `pixie trace` 运行应用程序的方式。可以把它想象成真实用户的程序化替身:它启动应用、发送请求,然后让应用自行处理。评估框架会为每个测试用例调用 `run()` 方法,传入用户的输入参数。应用程序通过其真实代码——真实的路由、真实的提示词组装、真实的 LLM 调用、真实的响应格式化——来处理这些参数,而评估框架则通过第 2a 步中的 `wrap()` 插桩来观察发生的一切。
|
||
|
||
**这意味着 Runnable 应该很简单。** 它只是将应用程序的真实入口点连接到评估框架的接口。如果你的 Runnable 变得越来越复杂——比如你在构建自定义逻辑、重新实现应用行为、或替换组件——那一定是有问题的。
|
||
|
||
## 四项要求
|
||
|
||
### 1. 运行真实的生产代码
|
||
|
||
Runnable 调用应用程序的实际入口点——也就是真实用户会触发的同一个函数、类或端点。它不会重新实现、偷工减料或替代应用程序的任何部分。
|
||
|
||
这包括 LLM。应用程序的 LLM 调用必须经过真实的代码路径——不要模拟、伪造或替换应用组件。基于评估的测试的全部意义就在于 LLM 输出是非确定性的,因此你要使用评估器(而不是断言)来打分。如果你用伪造品替换了任何组件,你就消除了真实行为,评估也就毫无意义。
|
||
|
||
**如果应用因无法解决的环境变量或配置缺失而无法运行,请停止并向用户反馈,让其修复环境配置。** 不要通过模拟组件来绕过问题。
|
||
|
||
### 2. 用 Pydantic BaseModel 表示启动参数
|
||
|
||
`run()` 方法接收一个 Pydantic `BaseModel`,其字段从数据集的 `input_data` 中填充。定义包含应用所需字段的子类:
|
||
|
||
```python
|
||
from pydantic import BaseModel
|
||
|
||
class AppArgs(BaseModel):
|
||
user_message: str
|
||
# 根据应用入口点的需要添加更多字段。
|
||
# 这些字段与数据集的 input_data 键一一对应。
|
||
```
|
||
|
||
**字段必须反映真实用户实际提供的内容。** 请阅读 `pixie_qa/00-project-analysis.md`——其中的「真实输入特征」部分描述了真实输入的复杂度、规模与多样性。设计模型时应使其能够接受该级别的输入,而非简化的玩具版本。
|
||
|
||
理解用户提供的参数与世界数据之间的边界:
|
||
|
||
- **用户提供的参数**(BaseModel 上的字段):真实用户输入或配置的内容——提示词、查询、配置标志、URL、模式定义。
|
||
- **世界数据**(由第 2a 步中的 `wrap(purpose="input")` 处理):应用在执行过程中从外部来源获取的内容——网页、数据库记录、API 响应。这不属于 BaseModel。
|
||
|
||
| 应用类型 | BaseModel 字段(用户提供) | 世界数据(wrap 提供) |
|
||
| ------------------ | ---------------------------------- | ----------------------------------------- |
|
||
| 网页爬虫 | URL + 提示词 + 模式定义 | HTML 页面内容 |
|
||
| 研究代理 | 研究问题 + 范围约束 | 源文档、搜索结果 |
|
||
| 客户支持机器人 | 客户的口头消息 | CRM 中的客户资料、会话存储中的对话历史 |
|
||
| 代码审查工具 | PR URL + 审查标准 | 实际的 diff、文件内容、CI 结果 |
|
||
|
||
如果某个字段最终保存的是应用本来会自行获取的数据,那它可能应该放在 `wrap(purpose="input")` 调用中,而不是 BaseModel 上。
|
||
|
||
### 3. 线程安全
|
||
|
||
`run()` 会被并发调用以处理多条数据集条目(最多并行 4 条)。如果应用使用了共享可变状态——SQLite、基于文件的数据库、全局缓存——请使用 `asyncio.Semaphore` 保护访问:
|
||
|
||
```python
|
||
import asyncio
|
||
|
||
class AppRunnable(pixie.Runnable[AppArgs]):
|
||
_sem: asyncio.Semaphore
|
||
|
||
@classmethod
|
||
def create(cls) -> "AppRunnable":
|
||
inst = cls()
|
||
inst._sem = asyncio.Semaphore(1)
|
||
return inst
|
||
|
||
async def run(self, args: AppArgs) -> None:
|
||
async with self._sem:
|
||
await call_app(args.message)
|
||
```
|
||
|
||
仅在应用确实存在共享可变状态时才添加信号量。如果应用使用基于请求的状态(以唯一 ID 为键)或本质上是无状态的,那么并发调用自然就是隔离的。
|
||
|
||
### 4. 遵循 Runnable 接口
|
||
|
||
```python
|
||
class AppRunnable(pixie.Runnable[AppArgs]):
|
||
@classmethod
|
||
def create(cls) -> "AppRunnable": ... # 构造实例
|
||
async def setup(self) -> None: ... # 一次,在第一次 run() 之前
|
||
async def run(self, args: AppArgs) -> None: ... # 每条数据集条目,并发执行
|
||
async def teardown(self) -> None: ... # 一次,在最后一次 run() 之后
|
||
```
|
||
|
||
- `create()` —— 类方法,返回一个新实例。使用带引号的返回类型(`-> "AppRunnable"`)以避免前向引用错误。
|
||
- `setup()` —— 可选的异步方法;初始化共享资源(HTTP 客户端、数据库连接、服务器)。
|
||
- `run(args)` —— 异步方法;每条数据集条目调用一次。在此处调用应用的真正入口点。
|
||
- `teardown()` —— 可选的异步方法;清理 `setup()` 中的资源。
|
||
|
||
## 最小示例
|
||
|
||
```python
|
||
# pixie_qa/run_app.py
|
||
from pydantic import BaseModel
|
||
import pixie
|
||
|
||
|
||
class AppArgs(BaseModel):
|
||
user_message: str
|
||
|
||
|
||
class AppRunnable(pixie.Runnable[AppArgs]):
|
||
"""驱动应用程序以进行追踪和评估。"""
|
||
|
||
@classmethod
|
||
def create(cls) -> "AppRunnable":
|
||
return cls()
|
||
|
||
async def run(self, args: AppArgs) -> None:
|
||
from myapp import handle_request
|
||
await handle_request(args.user_message)
|
||
```
|
||
|
||
仅此而已。Runnable 导入应用的真正入口点并调用它。没有自定义逻辑、没有组件替换、没有巧妙变通。
|
||
|
||
## 架构特定示例
|
||
|
||
根据应用程序的运行方式,阅读对应的示例文件:
|
||
|
||
| 应用类型 | 入口点 | 示例文件 |
|
||
| ----------------------------------- | ---------------------- | ------------------------------------------------------------ |
|
||
| **独立函数**(无服务器) | Python 函数 | 阅读 `references/runnable-examples/standalone-function.md` |
|
||
| **Web 服务器**(FastAPI、Flask) | HTTP/WebSocket 端点 | 阅读 `references/runnable-examples/fastapi-web-server.md` |
|
||
| **CLI 应用程序** | 命令行调用 | 阅读 `references/runnable-examples/cli-app.md` |
|
||
|
||
只阅读与你应用类型匹配的那个示例文件。
|
||
|
||
## 文件放置
|
||
|
||
- 将文件放在 `pixie_qa/run_app.py`。
|
||
- 数据集中的 `"runnable"` 字段引用:`"pixie_qa/run_app.py:AppRunnable"`。
|
||
- 项目根目录会自动加入 `sys.path`,因此可以使用正常的导入方式(`from app import service`)。
|
||
|
||
## 技术说明
|
||
|
||
不要在 runnable 文件中使用 `from __future__ import annotations`——它会破坏 Pydantic 对嵌套模型的模型解析。在需要的地方改用带引号的返回类型。
|
||
|
||
---
|
||
|
||
## 输出
|
||
|
||
`pixie_qa/run_app.py` —— Runnable 类。
|