Python `Annotated` 与 LangGraph Reducer 学习笔记

学习目标:

理解三个问题:

  1. Annotated 到底是什么?
  2. LangGraph 为什么要使用 Annotated
  3. Reducer 到底是什么,它是如何工作的?

# # 一、为什么会有 **Annotated****?**

Python 本身有类型提示(Type Hint):

makefile 复制代码
name: str
age: int
scores: list[int]

这些类型提示主要有两个作用:

  • 帮助 IDE 自动补全
  • 帮助静态类型检查(mypy、pyright)

但是,仅仅知道一个变量是 intlistdict,很多框架是不够的。

例如:

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 字段时,应该如何合并旧值和新值。
相关推荐
韩师傅1 小时前
海天线算法的前世今生
python·计算机视觉
韩师傅1 小时前
当你的甲方设备过烂,要如何快速出效果?
python·计算机视觉
Warson_L1 小时前
LangGraph的MessageState and HumanMessage
python
韩师傅2 小时前
当你的甲方吐槽天空不够蓝,你应该如何应对
python·计算机视觉
Warson_L2 小时前
python的类&继承
python
Warson_L2 小时前
类型标注/type annotation
python
ThreeS5 小时前
手搓MiniVLA全实战教程-一步一步用pytorch解释原理与思路
人工智能·python
金銀銅鐵6 小时前
[Python] 模 n 乘法的逆元计算器
python·数学·游戏