数据截至 (上游 commit eaf1040642b2)
高层门面:cast / classify / extract / generate / fn 如何都归约成一个 Task
30 秒导读: Marvin 对外那五个"一行就能用"的便捷函数——
cast(转类型)、classify(分类)、extract(抽实体)、generate(造数据)、@fn(预测函数输出)——没有各自的引擎。每一个都只做同一件事:拼一个带专用DEFAULT_PROMPT的Task,设好result_type,然后await task.run_async()。 本章讲清这条共同暗线,再逐个点出各自把"想要什么"翻译成result_type时的取舍。
本章只讲高层门面和支撑它们的 prompt 系统。底层机制(Task 怎么跑、result_type 怎么变成类型安全的结果、编排器怎么驱动一个回合)在前几章:
- 01-task-and-result-type.md ——
Task与类型安全结果:本章反复new Task(...).run_async(),那个Task是什么、result_type如何约束 LLM 输出,看这章。 - 02-orchestrator-turn-loop.md ——
run_async()背后的回合循环。 - 03-end-turn-and-agentlet.md —— 把"结束回合、交付结果"做成类型化工具。
- 04-actors-threads-scaling.md ——
agent/thread/handlers这些本章一路透传的参数,含义在这。
1. 这是什么(零基础也能懂)
一句话定义
Marvin 的"Keep it Simple"层:五个普通函数,把最常见的五种"让 LLM 干结构化活"包成一行调用,你不用碰 Task、不用写 prompt、不用想怎么解析输出。
解决什么问题 / 给谁用
假设你手上有一段脏文本"three dollars fifty",你想要一个 float 3.5;或者你有一句用户评论,想知道它是 "positive" 还是 "negative";或者你想凭空造 10 个假用户对象来测试。这些活的共同点是:输入是自然语言/任意数据,输出必须是一个确定类型的 Python 值。裸调 LLM 会得到一段字符串,还要自己解析、校验、重试。
Marvin 把这五种活各配一个函数:
| 函数 | 干什么 | 一行例子(示意,来自各文件 docstring) |
|---|---|---|
cast | 把数据转成一个目标类型的值 | cast("three point five", float) → 3.5 |
classify | 把数据归类到给定标签之一(或多个) | classify("red car", ["red","blue"]) → 'red' |
extract | 从数据里抽出一串某类型实体 | extract("$3.50 and €2", float) → [3.5, 2.0] |
generate | 按描述造 N 个结构化对象 | generate(str, n=3, instructions="fruits") |
@fn | 装饰一个函数,预测它的输出而不执行它 | 见 §3.5 |
一句话直觉
把 LLM 当成一个"万能类型转换器"。 你只要声明"我要什么类型",剩下的"怎么让模型吐出这个类型、怎么解析回来"都由底下那个 Task 兜住。这五个函数的全部差别,就在于它们各自怎么把"我要什么"翻译成一个 result_type。
2. 顶层全景(它大概怎么转)
共同暗线:五合一
先看这张图——这是本章唯一要记住的结构。五个函数看似做五种事,却在中段汇成同一条路:
cast(...) ┐
classify(...) │ 各自:
extract(...) ├─→ ① 选定自己的 DEFAULT_PROMPT(五份不同的系统提示)
generate(...) │ ② 把入参翻译成一个 result_type(这一步是差异所在)
@fn 装饰的函数 ┘ ③ 把 data / n / 参数塞进 task 的 context
│
▼
task = marvin.Task[T](
instructions = prompt,
context = {...},
result_type = <翻译出来的类型>,
)
│
▼
await task.run_async(thread, handlers) ←── 机制见第 1~3 章
│
▼
类型安全的结果 T (int / Enum / list[T] / …)
怎么读这张图: 从上往下是"五个入口 → 汇成一个 Task → 一次 run"。左边五个入口各不相同,汇合后完全共用下半段。所以"高层函数"其实不是引擎,而是一层薄薄的翻译器:把人类友好的签名翻译成 Task 的三件套(instructions、context、result_type)。
逐个印证这条暗线
每个文件都短到几乎一眼看穿。核心几行几乎逐字相同,只有 result_type 那行不一样:
| 函数 | 核心构造(简化自源码) | result_type 是什么 | 源码锚点 |
|---|---|---|---|
cast | Task(result_type=target) | 目标类型本身 | fns/cast.py:91 |
classify | Task(result_type=Labels(...) 或 labels) | 标签→整数索引 | fns/classify.py:150 |
extract | Task(result_type=list[target]) | 目标类型的 list | fns/extract.py:76 |
generate | Task(result_type=conlist(_target,min=n,max=n)) | 定长 list | fns/generate.py:84 |
@fn | Task(result_type=model.return_annotation) | 反射出来的返回注解 | fns/fn.py:88 |
同步/异步的对称结构
每个文件都有一对孪生函数:xxx_async(真正干活)和 xxx(同步壳)。同步版一律只是把异步版丢进 run_sync,零逻辑:
# 真实源码 fns/cast.py:145-156,符号 cast
def cast(data, target=None, ...):
return run_sync(cast_async(data=data, target=target, ...))
run_sync 来自 utilities/asyncio,负责在有无事件循环的场景下都能同步跑一个协程。记住这个模式:下文只讲 _async 版,同步版都是这一句话包一层。
3. 逐个门面:各自的取舍
共同暗线讲完了。真正有意思的是每个函数在"把入参翻成 result_type"这一步各自踩的坑和做的取舍。下面一节一个。
3.1 cast —— 转成"一个"目标类型,且 str 必须给指令
cast 是最纯粹的一个:result_type 就是你给的 target,不做任何包装。
# 真实源码 fns/cast.py:79-99,符号 cast_async(节选)
if target is None:
target = str
if target is str and instructions is None:
raise ValueError("Instructions are required when casting to string values.")
...
task = marvin.Task[target](
name="Cast Task",
instructions=prompt, # DEFAULT_PROMPT (+ instructions)
context=task_context, # {"Data to transform": data}
result_type=target, # ← 就是目标类型本身
)
return await task.run_async(thread=thread, handlers=handlers)
关键取舍:casting 到 str 必须给 instructions(fns/cast.py:82-83)。 为什么?因为 cast("hello", str) 语义上是空操作——你给它字符串、要它还字符串,不给方向的话模型不知道该"转"成什么。所以库在入口就抛 ValueError 逼你写清意图(比如 instructions="翻译成法语")。这是一个"前置校验代替运行期困惑"的设计:宁可在 0 成本处报错,也不发一次没意义的 LLM 请求。
DEFAULT_PROMPT(fns/cast.py:17)是"专家数据转换器"人设,还夹带了几条很具体的规则,比如"整数不要写小数""'3 dollars fifty cents' 转 float 要变 3.5""转 bool 时 truthy 值算 true"。这些规则不是通用的,是 cast 独有的——每个函数的 DEFAULT_PROMPT 都是为它那种活量身写的。
instructions 的拼接方式五个函数一致:不覆盖 DEFAULT_PROMPT,而是追加在后面(fns/cast.py:88-89):
prompt = prompt or DEFAULT_PROMPT
if instructions:
prompt += f"\n\nYou must follow these instructions for your transformation:\n{instructions}"