学习目标:
理解三个问题:
Annotated到底是什么?- LangGraph 为什么要使用
Annotated? - Reducer 到底是什么,它是如何工作的?
# # 一、为什么会有 **Annotated****?**
Python 本身有类型提示(Type Hint):
makefile
name: str
age: int
scores: list[int]
这些类型提示主要有两个作用:
- 帮助 IDE 自动补全
- 帮助静态类型检查(mypy、pyright)
但是,仅仅知道一个变量是 int、list、dict,很多框架是不够的。
例如:
FastAPI 希望知道:
这个参数最大长度是多少?
Pydantic 希望知道:
这个字段必须大于 0。
LangGraph 希望知道:
这个 State 更新的时候应该怎么合并?
这些信息都不是类型。
于是 Python 在 PEP 593 中引入了:
typing.Annotated
它的作用只有一句话:
给一个类型附加任意元数据(metadata)。
语法:
css
Annotated[Type, metadata1, metadata2, ...]
例如:
java
from typing import Annotated
Age = Annotated[int, "must >18"]
这里:
真正的类型:
arduino
int
元数据:
arduino
"must >18"
Python 不会理解 "must >18"。
Python 只是负责:
帮你保存起来。
因此:
Annotated = 类型 + 元数据
二、Python 本身不会解析 Annotated
这是整个知识点最重要的一件事情。
很多人第一次看到:
csharp
Annotated[int, add]
都会觉得:
Python 会不会自动调用 add?
答案:
不会。
Python 什么都不会做。
它只是保存:
csharp
Annotated
│
├── 真正类型:int
└── metadata:add
就结束了。
Python 根本不知道:
- add 是函数
- Query 是什么
- Field 是什么
- reducer 是什么
这些都是框架定义的。
可以把 Python 理解成:
一个负责存档的人。
而真正阅读档案的是:
- FastAPI
- Pydantic
- LangGraph
三、Annotated 内部到底保存了什么?
例如:
java
from typing import Annotated
Age = Annotated[
int,
"positive",
"required"
]
实际上里面保存的是:
arduino
Annotated
│
├── int
├── "positive"
└── "required"
可以使用:
javascript
from typing import get_origin
from typing import get_args
查看。
例如:
python
from typing import Annotated
from typing import get_origin
from typing import get_args
Age = Annotated[
int,
"positive",
"required"
]
print(get_origin(Age))
print(get_args(Age))
输出:
arduino
typing.Annotated
(
int,
"positive",
"required"
)
因此:
css
args[0]
永远是真正类型。
后面:
css
args[1:]
全部都是 metadata。
例如:
csharp
Annotated[
list,
add,
Cache(),
"important"
]
得到:
csharp
(
list,
add,
Cache(),
"important"
)
Python 不会解释:
- add
- Cache()
- important
只是全部保存下来。
四、框架是如何解析 Annotated 的?
真正解析 Annotated 的,是框架。
例如 LangGraph。
假设:
csharp
from operator import add
from typing import Annotated
from typing_extensions import TypedDict
class State(TypedDict):
messages: Annotated[list, add]
score: int
框架首先需要读取 State。
一般会使用:
ini
from typing import get_type_hints
hints = get_type_hints(
State,
include_extras=True
)
这里:
## ## 为什么一定要 **include_extras=True****?**
这是很多人容易忽略的一点。
默认:
scss
get_type_hints(State)
得到:
arduino
{
"messages": list,
"score": int
}
所有 metadata 都丢失了。
因为:
默认行为认为:
metadata 不影响类型检查。
所以自动去掉。
只有:
ini
include_extras=True
Python 才会保留:
csharp
{
"messages": Annotated[list, add],
"score": int
}
框架才能继续解析。
然后:
ini
for field_name, annotation in hints.items():
if get_origin(annotation) is Annotated:
args = get_args(annotation)
real_type = args[0]
metadata = args[1:]
对于:
csharp
messages: Annotated[list, add]
最终得到:
csharp
field_name
↓
messages
real_type
↓
list
metadata
↓
(add,)
框架随后就可以:
csharp
reducers["messages"] = add
保存下来。
因此:
Annotated 自己什么都没做。
真正工作的,是:
框架读取 metadata。
五、LangGraph 为什么需要 Annotated?
LangGraph 有一个核心概念:
State
例如:
kotlin
class State(TypedDict):
messages: list
每个 Node:
都可以返回:
kotlin
return {
"messages": ...
}
LangGraph 会更新 State。
问题来了。
假设:
旧 State:
json
{
"messages": [
"Hi"
]
}
Node 返回:
json
{
"messages": [
"Hello"
]
}
最后:
messages 应该变成:
css
方案一:
["Hello"]
还是:
css
方案二:
["Hi", "Hello"]
Python 根本不知道。
所以:
LangGraph 必须让开发者告诉它:
更新规则是什么。
于是:
csharp
messages: Annotated[list, add]
出现了。
这里:
list
表示:
类型。
csharp
add
表示:
更新规则。
六、Reducer 到底是什么?
Reducer:
本质就是:
Merge Strategy(合并策略)
或者:
State 更新策略
它的定义非常简单:
scss
(old_value, new_value)
↓
merged_value
例如:
python
def reducer(old, new):
...
返回:
新的 State。
例如:
最经典:
csharp
from operator import add
实际上:
css
add(a, b)
就是:
css
a + b
例如:
旧:
css
["Hi"]
新:
css
["Hello"]
Reducer:
css
add(
["Hi"],
["Hello"]
)
结果:
css
["Hi", "Hello"]
如果没有 reducer:
LangGraph 默认:
覆盖
即:
ini
result = new_value
最终:
css
["Hello"]
七、为什么 Reducer 很重要?
因为:
LangGraph 是一个 Graph。
不是:
Node1
↓
Node2
↓
Node3
它支持:
markdown
START
│
Retrieve Docs
/ \
Search DB Search Web
\ /
Merge Result
│
END
其中:
sql
Search DB
和:
sql
Search Web
可能:
同时运行。
同时返回:
json
{
"documents": [...]
}
例如:
Search DB:
json
{
"documents": [
"doc1",
"doc2"
]
}
Search Web:
json
{
"documents": [
"doc3"
]
}
如果没有 reducer:
LangGraph 就会遇到一个问题:
到底:
保留:
doc1
doc2
还是:
doc3
还是:
全部保留?
Reducer 就是解决:
多个更新如何合并。
于是:
csharp
documents: Annotated[
list,
add
]
最终:
sql
old + new
得到:
css
[ "doc1", "doc2", "doc3"]
八、Reducer 可以自己写
Reducer 其实就是普通函数。
例如:
保留最大值:
sql
def keep_max(old, new):
return max(old, new)
然后:
arduino
score: Annotated[
int,
keep_max
]
更新:
80
↓
90
得到:
90
如果:
95
↓
90
得到:
95
例如:
聊天记录去重:
sql
def unique_messages(old, new):
return list(
dict.fromkeys(
old + new
)
)
然后:
ini
messages: Annotated[
list,
unique_messages
]
即可自动去重。
因此:
Reducer:
其实只是:
sql
(old, new)
↓
merge(old, new)
九、FastAPI、Pydantic 与 LangGraph 的共同思想
FastAPI:
css
Annotated[ str, Query(max_length=20)]
Pydantic:
perl
Annotated[
int,
Field(gt=0)
]
LangGraph:
csharp
Annotated[
list,
add
]
它们都遵循同一个模式:
Python
↓
保存 metadata
↓
框架读取 metadata
↓
框架赋予 metadata 业务含义
区别只是:
| 框架 | Metadata 示例 | 框架赋予的含义 |
|---|---|---|
| FastAPI | Query(max_length=20) |
请求参数校验与 OpenAPI 描述 |
| Pydantic | Field(gt=0) |
数据验证规则 |
| LangGraph | add |
State 更新时的 reducer(合并策略) |
也就是说:
Python 并不知道:
scss
Query()
是什么。
也不知道:
scss
Field()
是什么。
更不知道:
csharp
add
为什么表示 reducer。
这些全部都是:
框架自己的约定(Convention)。
十、整套流程总结(核心)
ini
开发者
↓
messages: Annotated[list, add]
↓
Python
↓
保存:
(list, add)
↓
LangGraph
↓
get_type_hints(include_extras=True)
↓
发现:
messages
↓
Annotated[list, add]
↓
get_args()
↓
(
list,
add
)
↓
LangGraph 解释:
add 是 reducer
↓
保存:
reducers["messages"] = add
↓
以后 State 更新:
old_value = ["Hi"]
new_value = ["Hello"]
↓
调用:
add(old_value, new_value)
↓
得到:
["Hi", "Hello"]
一句话总结
Annotated是 Python 提供的"元数据容器",负责把额外信息附着到类型上,但不会解释这些信息。- 框架(如 LangGraph、FastAPI、Pydantic)负责读取
Annotated中的 metadata,并赋予它业务含义。 - 在 LangGraph 中,
Annotated最常见的用途就是指定Reducer------即多个节点更新同一个 State 字段时,应该如何合并旧值和新值。