前面已经使用 LangGraph 编写了固定路径、条件分支和并行分支。但是有一类任务不是执行一次就能结束:模型先生成内容,评估器再按照明确标准检查;如果没有通过,就把具体反馈交回生成器继续修改。
这种模式叫作 Evaluator-optimizer Workflow,也就是评估器优化器工作流。它的核心不是简单地多调用几次模型,而是让每次重试都有上一轮的评估依据:
生成内容
-> 评估内容
-> 通过则结束
-> 不通过则携带反馈重新生成
本文使用一个简单的中文冷笑话案例,把 State、结构化输出、条件路由、循环和停止条件组合起来。为了避免把图结构问题与模型问题混在一起,先使用确定性函数验证工作流,再接入本地 Qwen3。
1. 评估器优化器解决什么问题
普通模型调用只生成一次。结果是否满足要求,只能由调用方在工作流之外判断。
评估器优化器会把判断过程放进工作流:
- 生成器根据主题生成内容。
- 评估器按照预先定义的标准检查内容。
- 通过时结束工作流。
- 不通过时返回具体反馈。
- 生成器读取反馈并修改内容。
- 达到最大次数仍未通过时停止。

图中的 accepted、retry 和 max_attempts 是路由函数返回的业务值,不是三个节点。path_map 将两个停止结果 accepted 和 max_attempts 都映射到同一个 finish 节点,只有 retry 返回 generate_joke 继续生成。
它适合有明确验收标准、并且允许反复修改的任务,例如标题改写、格式检查、内容完整性检查和代码审查建议。
但它并不适合所有问题。如果标准无法说清楚,评估器就难以稳定判断;如果一次生成已经足够,多一次评估只会增加时间和调用成本。
这一执行模式与 LangGraph Evaluator-optimizer Workflow 的定义一致:生成器产生结果,评估器给出判断和反馈,未通过时再根据反馈重新生成。
2. 它与普通调用和 Agent 有什么区别
三种执行方式的差异主要在“下一步由谁决定”。
2.1 普通模型调用
程序提交输入,模型返回一次结果,流程随即结束:
输入 -> 模型 -> 输出
2.2 评估器优化器
程序员提前定义生成、评估、接受、重试和停止路径。模型负责生成内容和执行评估,但不会自由选择工作流之外的动作。
2.3 Agent
Agent 通常由模型根据当前消息决定下一步,例如调用哪个工具、继续思考还是返回最终答案。它比固定 Workflow 灵活,但执行路径也更难预测。
本文的流程属于 Workflow。它存在循环,但每条边和每个停止条件都由代码提前定义。
3. 本文的“评估器”指什么
这里的评估器是工作流中的一个运行时节点。用户每次执行工作流时,它都会检查本轮刚刚生成的内容。
它不是下面这些概念:
- 不是 LangSmith 中针对数据集批量运行的离线评估。
- 不是模型排行榜或基准测试。
- 不是人工标注平台。
这些功能都可能使用“评估”一词,但运行位置和目标不同。本文只讨论 Workflow 内部的在线评估节点。
4. 设计工作流 State
冷笑话工作流需要保存主题、当前笑话、评估反馈和循环次数:
class JokeState(TypedDict):
topic: str
joke: NotRequired[str]
feedback: NotRequired[str]
grade: NotRequired[Literal["funny", "not funny"]]
attempts: NotRequired[int]
stop_reason: NotRequired[str]
各字段的作用如下:
- topic:用户指定的主题。
- joke:生成器本轮返回的字符串。
- feedback:评估器的判断理由或修改建议。
- grade:固定为 funny 或 not funny。
- attempts:已经生成了多少次。
- stop_reason:最终因为通过还是达到上限而停止。

这些字段使用默认覆盖行为即可。新一轮笑话应该替换上一轮笑话,新一轮反馈也应该替换旧反馈,因此不需要列表 Reducer。
评估模型返回的 Feedback 对象不会直接交给路由函数。evaluate_joke 先取出其中的 grade 和 feedback,以局部更新的形式写回 State;LangGraph 合并更新后,路由函数再从 State 读取 grade 和 attempts。路由函数只选择路径,不修改 State。
5. 先用确定性函数验证循环
接入模型之前,先让生成器和评估器返回固定结果:
- 第一次生成普通陈述,固定评估为不通过。
- 第二次根据反馈生成问答式笑话,固定评估为通过。
- 当最大次数为 1 时,不允许进入第二次生成。
这样可以稳定验证条件路由,而不会受模型输出变化影响。
完整代码如下:
# 本示例不调用大模型,用固定结果演示评估器优化器的循环、反馈和停止条件。
from typing import Literal, NotRequired, TypedDict
from langgraph.graph import END, START, StateGraph
class JokeState(TypedDict):
"""定义冷笑话工作流在各节点之间传递的状态。"""
topic: str
joke: NotRequired[str]
feedback: NotRequired[str]
grade: NotRequired[Literal["funny", "not funny"]]
attempts: NotRequired[int]
max_attempts: int
stop_reason: NotRequired[str]
def generate_joke(state: JokeState) -> dict:
"""第一次生成普通句子,收到反馈后生成带有反转的冷笑话。"""
attempts = state.get("attempts", 0) + 1
if state.get("feedback"):
joke = "程序员去海边为什么带键盘?因为他想按一下浪(浪)。"
else:
joke = "程序员正在工作。"
return {"joke": joke, "attempts": attempts}
def evaluate_joke(state: JokeState) -> dict:
"""用固定规则模拟评估器,方便稳定观察循环行为。"""
if state["attempts"] >= 2:
return {"grade": "funny", "feedback": "笑话包含问答结构和反转,可以通过。"}
return {
"grade": "not funny",
"feedback": "这只是一句陈述,请增加问答结构和结尾反转。",
}
def route_after_evaluation(
state: JokeState,
) -> Literal["accepted", "retry", "max_attempts"]:
"""根据评估结果和生成次数决定结束还是重试。"""
if state["grade"] == "funny":
return "accepted"
if state["attempts"] >= state["max_attempts"]:
return "max_attempts"
return "retry"
def finish(state: JokeState) -> dict:
"""写入稳定的停止原因,便于调用方判断工作流如何结束。"""
stop_reason = "accepted" if state["grade"] == "funny" else "max_attempts"
return {"stop_reason": stop_reason}
builder = StateGraph(JokeState)
builder.add_node("generate_joke", generate_joke)
builder.add_node("evaluate_joke", evaluate_joke)
builder.add_node("finish", finish)
builder.add_edge(START, "generate_joke")
builder.add_edge("generate_joke", "evaluate_joke")
builder.add_conditional_edges(
"evaluate_joke",
route_after_evaluation,
{
"accepted": "finish",
"retry": "generate_joke",
"max_attempts": "finish",
},
)
builder.add_edge("finish", END)
workflow = builder.compile()
def run_case(max_attempts: int) -> None:
"""运行一个案例,并打印每个节点返回的局部更新。"""
print(f"\n最大生成次数:{max_attempts}")
final_state: JokeState = {
"topic": "程序员",
"max_attempts": max_attempts,
}
for update in workflow.stream(final_state, stream_mode="updates"):
for node_name, node_update in update.items():
final_state.update(node_update)
print(f"{node_name}: {node_update}")
print(
"最终结果:",
{
"attempts": final_state["attempts"],
"grade": final_state["grade"],
"stop_reason": final_state["stop_reason"],
},
)
run_case(max_attempts=3)
run_case(max_attempts=1)
运行代码:
source .venv_langgraph/bin/activate
python langgraph/p11_evaluator_optimizer/01_deterministic_evaluator_loop.py
真实输出如下:
最大生成次数:3
generate_joke: {'joke': '程序员正在工作。', 'attempts': 1}
evaluate_joke: {'grade': 'not funny', 'feedback': '这只是一句陈述,请增加问答结构和结尾反转。'}
generate_joke: {'joke': '程序员去海边为什么带键盘?因为他想按一下浪(浪)。', 'attempts': 2}
evaluate_joke: {'grade': 'funny', 'feedback': '笑话包含问答结构和反转,可以通过。'}
finish: {'stop_reason': 'accepted'}
最终结果: {'attempts': 2, 'grade': 'funny', 'stop_reason': 'accepted'}
最大生成次数:1
generate_joke: {'joke': '程序员正在工作。', 'attempts': 1}
evaluate_joke: {'grade': 'not funny', 'feedback': '这只是一句陈述,请增加问答结构和结尾反转。'}
finish: {'stop_reason': 'max_attempts'}
最终结果: {'attempts': 1, 'grade': 'not funny', 'stop_reason': 'max_attempts'}
第一组案例证明反馈进入了第二轮生成。第二组案例证明达到次数上限后,工作流不会继续循环。
6. 使用 Pydantic 定义评估结果
如果让评估器返回一段自然语言,路由函数就需要自行判断“通过”“很好”“可以”等不同说法。模型措辞稍有变化,条件判断就可能失效。
更稳定的做法是定义结构化结果:
class Feedback(BaseModel):
grade: Literal["funny", "not funny"]
feedback: str
Literal 把等级限制为两个值,feedback 保存判断理由。路由函数只需要读取 grade,不再解析自然语言。
本地 Qwen3 通过 MLX-LM 提供 OpenAI 兼容接口。本文使用已经验证的 function_calling 方法:
evaluator = model.with_structured_output(
Feedback,
method="function_calling",
)
OpenAI 兼容接口只说明请求和响应格式相似,并不代表推理服务支持每一种 Structured Output 方法。本地服务不应在没有测试的情况下直接改用默认 json_schema。
7. 单独验证结构化评估器
在完整工作流中同时排查生成、评估和路由会比较困难。因此先用一条固定笑话验证评估器。
完整代码如下:
# 本示例使用本地 Qwen3 评估一条固定冷笑话,验证 Pydantic 结构化输出。
from typing import Literal
import httpx
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
class Feedback(BaseModel):
"""约束评估器只能返回固定等级和具体反馈。"""
grade: Literal["funny", "not funny"] = Field(
description="笑话是否达到基本的冷笑话标准"
)
feedback: str = Field(description="判断理由;不通过时给出具体修改建议")
# 禁用系统代理,确保本机请求直接发送到 MLX-LM。
http_client = httpx.Client(trust_env=False)
model = ChatOpenAI(
model="default_model",
base_url="http://127.0.0.1:18080/v1",
api_key="not-needed",
temperature=0,
max_tokens=256,
http_client=http_client,
)
# 本地 MLX-LM 已验证 function_calling,可以返回符合 Feedback 的参数。
evaluator = model.with_structured_output(Feedback, method="function_calling")
result = evaluator.invoke(
"""请评估下面的中文冷笑话。
评估标准:
1. 内容与主题相关;
2. 有清晰的双关、误解或结尾反转;
3. 表达完整,读者能够理解笑点。
笑话:程序员最讨厌哪一种声音?需求变更的提示音。
如果不通过,请给出一条可以直接执行的修改建议。"""
)
print("返回类型:", type(result).__name__)
print("评估等级:", result.grade)
print("评估反馈:", result.feedback)
http_client.close()
8. 启动本地 Qwen3 服务
进入 llm_learning 根目录,在独立终端启动模型服务:
source .venv_tool_server/bin/activate
"$VIRTUAL_ENV/bin/python" -m mlx_lm server \
--model model \
--host 127.0.0.1 \
--port 18080 \
--prompt-cache-size 0 \
--chat-template-args '{"enable_thinking": false}'
运行模型代码之前先检查健康接口:
curl --noproxy '*' http://127.0.0.1:18080/v1/models
再执行结构化评估脚本:
source .venv_langgraph/bin/activate
python langgraph/p11_evaluator_optimizer/02_test_structured_evaluator.py
本次真实输出为:
返回类型: Feedback
评估等级: funny
评估反馈: 该笑话符合冷笑话的标准:内容与主题相关,有清晰的结尾反转,并且表达完整,读者能够理解笑点。
返回类型是经过 Pydantic 校验的 Feedback,不是普通字典,也不是 AIMessage。
9. 编写生成节点
生成节点需要区分第一次生成和后续修改:
feedback = state.get("feedback")
if feedback:
instruction = f"请根据反馈重新生成:{feedback}"
else:
instruction = "请根据主题第一次生成"
如果节点每一轮都只读取 topic,而完全忽略 feedback,虽然图上形成了循环,内容却没有真正被优化。
另一个容易出错的地方是返回类型。ChatOpenAI.invoke() 返回 AIMessage,但 State 中的 joke 被定义为字符串,因此节点应该保存 response.content:
return {
"joke": str(response.content).strip(),
"attempts": attempts,
}
10. 编写评估节点
评估节点读取主题和当前笑话,再调用结构化模型:
result = structured_evaluator.invoke(...)
return {
"grade": result.grade,
"feedback": result.feedback,
}
评估标准需要尽可能具体。本文检查以下三项:
- 是否与主题相关。
- 是否有清晰的双关、误解或结尾反转。
- 中文表达是否完整,读者能否理解笑点。
如果只要求模型判断“好不好”,评估结果会缺少稳定依据,反馈也很难指导下一轮修改。
11. 编写条件路由
路由函数先检查是否通过,再检查最大次数:
def route_after_evaluation(state: JokeState):
if state["grade"] == "funny":
return "accepted"
if state["attempts"] >= MAX_ATTEMPTS:
return "max_attempts"
return "retry"
三个路由值通过 path_map 映射到真实节点:
builder.add_conditional_edges(
"evaluate_joke",
route_after_evaluation,
{
"accepted": "finish",
"retry": "generate_joke",
"max_attempts": "finish",
},
)
path_map 让路由含义和图中的节点名称分离。accepted 是业务结果,finish 才是真实节点。
12. 为什么必须限制最大次数
评估器可能连续拒绝生成内容。如果只有“通过才结束”这一条规则,工作流可能无限循环。
本文使用两层保护:
12.1 业务停止条件
MAX_ATTEMPTS = 3
三次生成后仍未通过,就把 stop_reason 写成 max_attempts 并结束。这是程序能够向调用方解释的业务结果。
12.2 LangGraph 递归限制
config={"recursion_limit": 20}
recursion_limit 是运行时最后一道保护。如果图因为路由错误一直循环,LangGraph 会抛出 GraphRecursionError。
它不能替代业务停止条件。递归异常只能说明图没有按预期结束,无法告诉调用方是评估未通过还是程序存在错误。
13. 完整评估器优化器 Workflow
下面把生成器、评估器、条件路由和结束节点组合起来:
# 本示例使用本地 Qwen3 完成冷笑话的生成、评估、反馈重试和停止判断。
from typing import Literal, NotRequired, TypedDict
import httpx
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import END, START, StateGraph
from pydantic import BaseModel, Field
MAX_ATTEMPTS = 3
class JokeState(TypedDict):
"""定义评估器优化器工作流中的共享状态。"""
topic: str
joke: NotRequired[str]
feedback: NotRequired[str]
grade: NotRequired[Literal["funny", "not funny"]]
attempts: NotRequired[int]
stop_reason: NotRequired[str]
class Feedback(BaseModel):
"""约束评估节点返回的等级和反馈。"""
grade: Literal["funny", "not funny"] = Field(
description="笑话是否达到基本的冷笑话标准"
)
feedback: str = Field(description="判断理由;不通过时给出具体修改建议")
# 两个模型客户端访问同一个 Qwen3 服务,但使用不同温度。
generator_http_client = httpx.Client(trust_env=False)
evaluator_http_client = httpx.Client(trust_env=False)
generator_model = ChatOpenAI(
model="default_model",
base_url="http://127.0.0.1:18080/v1",
api_key="not-needed",
temperature=0.7,
max_tokens=256,
http_client=generator_http_client,
)
evaluator_model = ChatOpenAI(
model="default_model",
base_url="http://127.0.0.1:18080/v1",
api_key="not-needed",
temperature=0,
max_tokens=256,
http_client=evaluator_http_client,
)
structured_evaluator = evaluator_model.with_structured_output(
Feedback,
method="function_calling",
)
def generate_joke(state: JokeState) -> dict:
"""根据主题生成笑话;重试时必须参考上一次评估反馈。"""
attempts = state.get("attempts", 0) + 1
feedback = state.get("feedback")
if feedback:
instruction = f"""请围绕“{state['topic']}”重新写一个简短的中文冷笑话。
上一版没有通过,评估反馈是:{feedback}
请针对反馈改进,使用问答结构、双关或结尾反转,只输出笑话正文。"""
else:
instruction = f"""请围绕“{state['topic']}”写一个简短的中文冷笑话。
笑话需要有问答结构、双关或结尾反转,只输出笑话正文。"""
response = generator_model.invoke(
[
SystemMessage(content="你是一名擅长写简短中文冷笑话的创作者。"),
HumanMessage(content=instruction),
]
)
# State 中的 joke 字段保存字符串,不直接保存 AIMessage 对象。
return {"joke": str(response.content).strip(), "attempts": attempts}
def evaluate_joke(state: JokeState) -> dict:
"""按照固定标准评估当前笑话,并返回 Pydantic 校验后的结果。"""
result = structured_evaluator.invoke(
f"""请评估下面的中文冷笑话。
主题:{state['topic']}
笑话:{state['joke']}
评估标准:
1. 内容与主题相关;
2. 有清晰的双关、误解或结尾反转;
3. 表达完整,读者能够理解笑点。
达到三项标准时 grade 返回 funny;否则返回 not funny,并给出一条可以直接执行的修改建议。"""
)
return {"grade": result.grade, "feedback": result.feedback}
def route_after_evaluation(
state: JokeState,
) -> Literal["accepted", "retry", "max_attempts"]:
"""评估通过则结束,否则在次数范围内返回生成节点。"""
if state["grade"] == "funny":
return "accepted"
if state["attempts"] >= MAX_ATTEMPTS:
return "max_attempts"
return "retry"
def finish(state: JokeState) -> dict:
"""记录工作流的真实停止原因。"""
stop_reason = "accepted" if state["grade"] == "funny" else "max_attempts"
return {"stop_reason": stop_reason}
builder = StateGraph(JokeState)
builder.add_node("generate_joke", generate_joke)
builder.add_node("evaluate_joke", evaluate_joke)
builder.add_node("finish", finish)
builder.add_edge(START, "generate_joke")
builder.add_edge("generate_joke", "evaluate_joke")
builder.add_conditional_edges(
"evaluate_joke",
route_after_evaluation,
{
"accepted": "finish",
"retry": "generate_joke",
"max_attempts": "finish",
},
)
builder.add_edge("finish", END)
workflow = builder.compile()
initial_state: JokeState = {"topic": "程序员"}
final_state = initial_state.copy()
print("节点更新:")
for update in workflow.stream(
initial_state,
config={"recursion_limit": 20},
stream_mode="updates",
):
for node_name, node_update in update.items():
final_state.update(node_update)
if node_name == "generate_joke":
print(f"- 第 {node_update['attempts']} 次生成:{node_update['joke']}")
elif node_name == "evaluate_joke":
print(f"- 评估:{node_update['grade']}")
print(f" 反馈:{node_update['feedback']}")
else:
print(f"- 停止原因:{node_update['stop_reason']}")
print("\n最终状态:")
print("主题:", final_state["topic"])
print("笑话:", final_state["joke"])
print("生成次数:", final_state["attempts"])
print("评估等级:", final_state["grade"])
print("停止原因:", final_state["stop_reason"])
generator_http_client.close()
evaluator_http_client.close()
运行代码:
source .venv_langgraph/bin/activate
python langgraph/p11_evaluator_optimizer/03_qwen3_evaluator_optimizer_workflow.py
14. 观察真实运行结果
本次运行中,生成器连续修改了三次,评估器仍然认为结尾反转不够清晰,因此工作流按次数上限停止:
节点更新:
- 第 1 次生成:为什么程序员喜欢用黑暗模式?
因为亮着的灯容易让他们“代码”出错。
- 评估:not funny
反馈:笑话中的双关不够清晰,可以修改为:为什么程序员喜欢用黑暗模式?因为亮着的灯容易让他们‘调试’出错。
- 第 2 次生成:为什么程序员喜欢用黑暗模式?因为亮着的灯容易让他们“调试”出错。
- 评估:not funny
反馈:笑话的结尾反转不够清晰,建议修改为:为什么程序员喜欢用黑暗模式?因为亮着的灯容易让他们‘调试’出错。
- 第 3 次生成:为什么程序员喜欢用黑暗模式?因为亮着的灯容易让他们“调试”出错。
- 评估:not funny
反馈:笑话的结尾反转不够清晰,建议修改为:为什么程序员喜欢用黑暗模式?因为亮着的灯容易让他们‘调试’出错。
- 停止原因:max_attempts
最终状态:
主题: 程序员
笑话: 为什么程序员喜欢用黑暗模式?因为亮着的灯容易让他们“调试”出错。
生成次数: 3
评估等级: not funny
停止原因: max_attempts
这不是程序执行失败,而是一个有效的业务结果:模型确实读取了反馈并重新生成,但三次之内没有达到评估器的标准。
模型输出存在非确定性。再次运行可能第一次就通过,也可能生成不同笑话。稳定的是 State 结构、路由规则、最大次数和停止原因。
15. 使用 stream 查看每轮更新
invoke() 适合直接取得最终 State:
result = workflow.invoke(
{"topic": "程序员"},
config={"recursion_limit": 20},
)
本文使用:
workflow.stream(..., stream_mode="updates")
updates 每次返回节点产生的局部更新,所以可以看到:
- 生成节点写入了哪一版笑话。
- 评估节点返回了什么等级和反馈。
- 工作流最终因为什么停止。
它不会自动返回一份累计 State,因此示例使用 final_state.update(node_update) 将每次局部更新合并起来,仅用于最后打印完整结果。
16. 模型调用次数与成本
每一轮通常包含两次模型调用:
- 生成器调用一次。
- 评估器调用一次。
如果生成了 N 轮,通常约有 2N 次模型调用。本文最多生成 3 次,因此最多调用模型 6 次。
实际系统还需要关注:
- 单轮生成和评估耗时。
- 最大重试次数。
- 每轮笑话与最新反馈的长度。
- 本地推理资源或在线 API 成本。
本文的 joke 和 feedback 都使用默认覆盖行为,只保留最新一轮内容,不会把历次反馈自动累积到 State。如果业务需要保存完整修改历史,应单独增加列表字段并配置合适的 Reducer。
不要为了追求“评估通过”无限增加重试次数。次数上限应该由任务价值、延迟要求和资源预算共同决定。
17. 评估器的局限
17.1 标准不明确会导致反馈不稳定
“写得更好”不是可执行标准。应尽量拆成主题相关性、格式完整性、字段齐全等可以判断的条件。
17.2 同一个模型可能存在自我评估偏差
本文为了本地演示,生成和评估都使用 Qwen3,只是温度不同。在要求更高的系统中,可以使用独立模型、规则校验或人工检查共同评估。
17.3 通过不代表事实一定正确
评估器只能按照提示词和自身能力判断。结构化输出保证了结果格式,不保证判断本身绝对正确。
17.4 优化器可能没有真正采纳反馈
本次实测中,第二次与第三次生成基本相同。这说明把反馈放进提示词只能提高模型采纳建议的可能性,不能保证每次都产生有效改进。
18. 常见问题
18.1 把 AIMessage 直接写入字符串字段
ChatOpenAI.invoke() 返回 AIMessage。如果 joke 定义为字符串,应写入 response.content,否则后续格式化提示词时会混入消息对象表示。
18.2 Pydantic 字段与模型结果不匹配
路由依赖 grade,Schema 和提示词中就必须使用同一个字段名。字段缺失或 Literal 值不合法会产生验证错误。
18.3 路由值与 path_map 不一致
路由函数返回 retry,path_map 中也必须存在 retry。拼写不同会导致图找不到下一节点。
18.4 生成节点没有读取反馈
图可以循环不代表内容会改进。重试提示词必须明确包含 state[“feedback”]。
18.5 没有最大循环次数
评估器可能永远返回 not funny。必须使用业务次数上限,并保留 recursion_limit 作为最后保护。
18.6 本地服务不支持默认 json_schema
with_structured_output() 的具体方法依赖模型服务。本文的 MLX-LM 环境使用已经实测通过的 method=”function_calling”。
18.7 本机请求被代理转发
如果 VPN 或系统代理影响 127.0.0.1 请求,可以像本文一样传入:
httpx.Client(trust_env=False)
同时先用 /v1/models 健康接口区分“服务没有启动”和“模型调用失败”。
19. 总结
| 知识点 | 作用 | 本文验证结果 |
|---|---|---|
| Evaluator-optimizer | 生成后评估,不通过则携带反馈重试 | 冷笑话最多生成三次 |
| JokeState | 保存主题、内容、反馈、等级和次数 | 每个节点只返回局部更新 |
| 生成节点 | 第一次按主题生成,后续按反馈修改 | 实测连续生成三版内容 |
| 评估节点 | 按明确标准判断当前结果 | 返回 Pydantic Feedback |
| Literal | 限制评估等级的合法值 | 只允许 funny 和 not funny |
| function_calling | 让本地 Qwen3 返回结构化参数 | MLX-LM 实测成功 |
| 条件边 | 根据评估结果选择结束或重试 | accepted、retry、max_attempts |
| path_map | 将业务路由值映射到真实节点 | 图结构包含完整分支 |
| stream_mode=”updates” | 查看每个节点的局部状态更新 | 可观察每轮生成与反馈 |
| 最大次数 | 防止评估器持续拒绝造成无限循环 | 三次后以 max_attempts 停止 |
| recursion_limit | 防止错误图无限执行 | 低上限测试触发 GraphRecursionError |
| 调用次数 | 每轮一次生成和一次评估 | N 轮通常约 2N 次模型调用 |
| 评估限制 | 结构正确不等于判断绝对正确 | 实测反馈未保证生成器有效改进 |
评估器优化器的价值不是保证模型一定写出完美结果,而是把“生成、检查、反馈和停止”变成一条可观察、可限制、可解释的 Workflow。
下一篇开始构建课程中的智能小秘书案例。首先处理模型一次生成多个 Tool Call 时的异步并发执行,再使用 ToolNode 简化工具节点。