
一、前言
本篇约1.2万字,阅读时长约30分钟。
随着LLM应用越来越复杂,单轮对话已经很难满足高质量输出的需求。一个常见的痛点是:LLM第一次生成的答案往往不够好——可能遗漏关键信息,可能逻辑不够严密,可能缺乏引用支撑。
那么问题来了:怎么让LLM"回头看看自己写的答案,想想哪里不对,然后改一改"?这就是Reflection模式要解决的事。而Reflexion模式更进一步——不光自己想,还去外面查。本文就来拆解这两种模式在LangGraph上的完整实现。
一、前言二、Reflection与Reflexion:两种反思范式三、Reflection模式源码剖析四、Reflexion模式源码剖析五、两种模式的工程对比六、要点总结
二、Reflection与Reflexion:两种反思范式
Reflection的核心思想很简单:让LLM自己批评自己。
具体来说,它引入了两个角色:
- 生成器(Generator):负责写内容
- 反思器(Reflector):负责挑毛病
生成器写完,反思器来批,批完生成器再改,如此循环。说白了就是让LLM瞎折腾自己的输出——但折腾完确实变好了。
Reflexion在Reflection的基础上,多了一层外部工具调用。它不光靠LLM自己"想",还会真正去搜索外部信息来验证和补充。
那么Reflexion相比Reflection多了什么:
- 结构化输出:通过
bind_tools强制LLM输出结构化的AnswerQuestion对象 - 工具调用:引入搜索工具,执行
search_queries拿回真实信息 - 情景记忆(Episodic Memory):每一轮的反思和搜索结果都累积到消息列表中,后续轮次能看到前面的"教训"
- 引用机制:修订后的答案必须带数字引用(
[1] https://...)
简单说,Reflection是"闭卷自我修改",Reflexion是"开卷查证修改"。
两者最核心的差异有三个:
- 输出方式:Reflection是纯文本,Reflexion是结构化(
bind_tools+ Pydantic) - 外部信息:Reflection纯靠LLM脑子里的知识,Reflexion会调
TavilySearchResults去外面查 - 终止条件:Reflection靠哨兵标记,Reflexion用双闸门
更详细的对比放在第五章。先把两个模式的源码逐行拆开。
三、Reflection模式源码剖析
先看整体拓扑,再逐层拆解。
Reflection拓扑:自我精炼的闭环

整条链路只有三个节点:generate、reflect、加一个条件路由should_continue。简单到不能再简单。
1、State设计:一条消息链承载全部记忆
State只维护一个字段——messages列表:
class State(TypedDict): messages: Annotated[list, add_messages]
add_messages是LangGraph内置的reducer,作用是:每轮generate和reflect产出的新消息会追加到列表尾部,而不是覆盖。
这意味着整个对话历史——请求、初稿、批评、修订稿、再批评——全部累积在一条messages链里。这就是"情景记忆"的载体。
2、Prompt设计:生成器与反思器各司其职
生成器和反思器的Prompt分别长这样:
generate_prompt = ChatPromptTemplate.from_messages([ ("system", "You are an essay assistant tasked with writing excellent 5-paragraph essays." " Generate the best essay possible for the user's request." " If the user provides critique, respond with a revised version."), MessagesPlaceholder(variable_name="messages"),])reflection_prompt = ChatPromptTemplate.from_messages([ ("system", "You are a teacher grading an essay submission." " Generate critique and recommendations for the user's submission." " If the submission needs NO further revision," " append the exact token <<NO_REVISIONS_NEEDED>>" " at the very end of your critique."), MessagesPlaceholder(variable_name="messages"),])
注意一个关键设计:反思器的Prompt里埋了一个哨兵标记<<NO_REVISIONS_NEEDED>>。当反思器认为"不用再改了",它会在批评末尾附带这个标记。这是一种确定性的终止信号——不靠关键词猜测,靠精确匹配。
3、generate节点:朴实无华的链调用
generate节点就是一个prompt | model的链调用,包了一层工厂函数:
def make_generation_node(generate) -> Callable[[State], dict]: def generation_node(state: State) -> dict: return {"messages": [generate.invoke(state["messages"])]} return generation_node
工厂函数的目的是让generate这个Runnable可注入——测试时可以传一个假的Runnable进来,不需要真正调用LLM。这是有必要的,否则单测没法跑。
4、reflect节点:角色翻转是精髓
reflect节点干了一件看起来很"诡异"的事——翻转消息角色:
def make_reflection_node(reflect) -> Callable[[State], dict]: def reflection_node(state: State) -> dict: cls_map = {"ai": HumanMessage, "human": AIMessage} # 保留原始请求,翻转之后的所有消息角色 translated = [state["messages"][0]] + [ cls_map[m.type](content=m.content) for m in state["messages"][1:] ] res = reflect.invoke(translated) return {"messages": [HumanMessage(content=res.content)]} return reflection_node
为什么要翻转?因为反思器的Prompt设定是"你是老师,在批改学生提交的作文"。而实际上之前的作文是AI写的(AIMessage)。
所以需要把AI写的作文伪装成HumanMessage(学生提交),让反思器以为自己在批改学生的作业。反过来,反思器输出的批评,又被包装成HumanMessage返回给生成器。生成器看到的是"用户给了反馈,让我改"。
角色翻转:一场精心编排的双簧 生成器(AI)写作文 → 翻转为 HumanMessage 反思器(AI)批改 → 输出包装为 HumanMessage 生成器收到反馈 → 以为用户要求修改
5、should_continue:什么时候停
终止逻辑只有两条规则,干净利落:
_SATISFIED_MARKER = "<<NO_REVISIONS_NEEDED>>"def should_continue(state: State) -> str: messages = state["messages"] if len(messages) > 6: # 兜底:消息数超限 return END last = messages[-1] if isinstance(last, HumanMessage) and _SATISFIED_MARKER in last.content: return END # 满意了,停 return "generate" # 不满意,回去继续改
个人认为,把检查放在reflect之后而不是generate之后,是一个很务实的改进。因为这样可以直接读到最新的批评,不需要去messages[-2]偷看前一条消息。
6、build_app:把零件拼起来
最后把所有节点和边组装成一张完整的图:
def build_app(*, model=None, generate=None, reflect=None, checkpointer=None): if generate is None or reflect is None: if model is None: model = make_default_model() if generate is None: generate = generate_prompt | model if reflect is None: reflect = reflection_prompt | model workflow = StateGraph(State) workflow.add_node("generate", make_generation_node(generate)) workflow.add_node("reflect", make_reflection_node(reflect)) workflow.add_edge(START, "generate") workflow.add_edge("generate", "reflect") workflow.add_conditional_edges("reflect", should_continue, ["generate", END]) return workflow.compile(checkpointer=checkpointer or InMemorySaver())
generate和reflect都是可选注入的。传了就用你传的,没传就从model推导。这种设计让整个图的每个角色都可替换——这是工程上靠谱的做法。
四、Reflexion模式源码剖析
Reflexion的拓扑明显更复杂,多了execute_tools节点:
Reflexion拓扑:带工具调用的反思闭环

关键区别在于:draft和revise是两个不同的角色(绑定不同的tool schema),中间还插了一个execute_tools来执行搜索。
1、Schema设计:用Pydantic锁死输出格式
Reflexion用Pydantic模型定义了三种结构化输出,强制LLM按schema吐数据:
class Reflection(BaseModel): """The actor's self-critique.""" missing: str = Field(description="Critique of what is missing.") superfluous: str = Field(description="Critique of what is superfluous.")class AnswerQuestion(BaseModel): """Answer the question: an answer, a reflection, and follow-up queries.""" answer: str = Field(description="~250 word detailed answer to the question.") reflection: Reflection = Field(description="Your reflection on the initial answer.") search_queries: list[str] = Field( description="1-3 search queries for researching improvements." )class ReviseAnswer(AnswerQuestion): """Revise the original answer, with citations motivating the changes.""" references: list[str] = Field(description="Citations motivating your updated answer.")
关键点在于:ReviseAnswer继承了AnswerQuestion并多加一个references字段。这意味着修订版必须带引用,而初始版不需要。
这些schema通过bind_tools绑定到LLM上,强制LLM以function call的形式输出结构化数据,而不是自由文本。
2、Prompt设计:一个模板复用两个角色
Reflexion的Prompt同时服务于初始回答和修订回答:
actor_prompt_template = ChatPromptTemplate.from_messages([ ("system", "You are expert researcher.\n" "Current time: {time}\n\n" "1. {first_instruction}\n" "2. Reflect and critique your answer. Be severe to maximize improvement.\n" "3. Recommend search queries to research information and improve your answer."), MessagesPlaceholder(variable_name="messages"), ("user", "\n\n<system>Reflect on the user's original question and the actions taken thus far." " Respond using the {function_name} function.</reminder>"),]).partial(time=lambda: datetime.datetime.now().isoformat())
first_instruction和function_name是可变参数:
- 初始回答时:
first_instruction="Provide a detailed ~250 word answer.",function_name=AnswerQuestion - 修订回答时:
first_instruction=revise_instructions(要求加引用),function_name=ReviseAnswer
一个模板复用两个角色,靠参数区分,这是务实的做法。
3、ResponderWithRetries:带着质检员的流水线
未经审视的人生不值得过。——苏格拉底
这是Reflexion模式里最值得品的设计。它不是简单地调用LLM,而是"调用-验证-失败重试"三步走,就像流水线末端站了一个质检员,不合格就打回去返工:
class ResponderWithRetries: def __init__(self, runnable, validator): self.runnable = runnable self.validator = validator def respond(self, state: State) -> dict: messages = state["messages"] response = None for attempt in range(3): # 最多重试3次 response = self.runnable.invoke( {"messages": messages}, {"tags": [f"attempt:{attempt}"]} ) try: self.validator.invoke(response) # PydanticToolsParser 验证 return {"messages": [response]} except ValidationError as e: # 把错误信息塞回去当ToolMessage,让LLM看着错误改 tool_call_id = ( response.tool_calls[0]["id"] if response.tool_calls else str(attempt) ) messages = messages + [ response, ToolMessage( content=( f"{repr(e)}\n\nPay close attention to the function" " schema.\n\nRespond by fixing all validation errors." ), tool_call_id=tool_call_id, ), ] return {"messages": [response]}
执行流程拆解:
- 调用
runnable拿到LLM响应 - 用
PydanticToolsParser验证输出是否符合schema - 验证通过,直接返回
- 验证失败,把错误信息塞回去当
ToolMessage,让LLM"看着错误自己改" - 最多重试3次,3次都不行就返回最后一次的结果
个人认为,这种"把验证错误反馈给模型"的设计比直接抛异常要靠谱得多。LLM看到ValidationError的具体描述后,往往能在下一轮修正格式问题。
注意一个原版notebook的Bug修复:原代码写的是state + [response, ToolMessage(...)],这是dict + list操作,直接抛TypeError。正确写法是messages + [response, ToolMessage(...)]——追加到列表,而不是用state做加法。
4、execute_tools:跑腿小弟
execute_tools:负责把LLM给出的搜索词拿去跑
工具节点负责执行LLM给出的search_queries:
def run_queries(search_queries: list[str], **kwargs): if os.getenv("TAVILY_API_KEY"): try: from langchain_community.tools.tavily_search import TavilySearchResults tavily = TavilySearchResults(max_results=5) return tavily.batch([{"query": q} for q in search_queries]) except ImportError: pass # 可选依赖缺失,走离线 return [_offline_lookup(q) for q in search_queries]def build_tool_node() -> ToolNode: return ToolNode([ StructuredTool.from_function(run_queries, name=AnswerQuestion.__name__), StructuredTool.from_function(run_queries, name=ReviseAnswer.__name__), ])
设计上有两个细节值得注意:
run_queries同时注册了两个名字——AnswerQuestion和ReviseAnswer。因为draft和revise两个节点的LLM输出的tool call名称不同,但实际执行的是同一个搜索函数- 没有配置
TAVILY_API_KEY时,返回离线占位文本。这让整个loop不需要网络也能跑起来,方便演示和调试
5、event_loop:两道水坝,谁先满谁开闸
流水不腐,户枢不蠹。但流太多了也得拦住。
Reflexion的终止条件是最复杂也最精妙的部分。它用了两道独立的闸门,就像两道水坝——哪道先溢出就触发泄洪(终止):
MAX_QUERIES = 12 # 所有轮次search_queries总和上限MAX_REVISIONS = 5 # 修订次数上限(不含初始draft)def event_loop(state: State) -> str: if ( _get_total_search_queries(state) > MAX_QUERIES or _get_revision_count(state) > MAX_REVISIONS ): return END return "execute_tools"
两道闸门是OR关系,谁先超就谁触发。
为什么需要两道?因为LLM的行为不可预测:
- 如果LLM每轮都吐出很长的
search_queries列表,查询总数会先爆——MAX_QUERIES闸门触发 - 如果LLM每轮只吐一两个查询,修订次数会先爆——
MAX_REVISIONS闸门触发 - 无论哪种情况,循环都不会无限跑下去
以下是一个假设场景(假设LLM每轮吐3条查询),展示闸门触发过程:
双闸门终止示意(假设LLM每轮吐3条查询)轮次 累计查询数 累计修订数 触发闸门 1 3 0 - 2 6 1 - 3 9 2 - 4 12 3 - 5 15 4 MAX_QUERIES > 12 → END
如果只有一道闸门,要么拦不住查得多的,要么拦不住改得多的。两道齐上,整得挺靠谱。
_get_revision_count的计算逻辑也需要留意:它统计的是带tool_calls的AIMessage数量减1。因为第一条AIMessage是初始draft,不计入修订次数。
6、build_app:组装带工具的反思图
def build_app(*, model=None, first_responder=None, revisor=None, tool_node=None, checkpointer=None): if first_responder is None or revisor is None: if model is None: model = make_default_model() if first_responder is None: initial_chain = actor_prompt_template.partial( first_instruction="Provide a detailed ~250 word answer.", function_name=AnswerQuestion.__name__, ) | model.bind_tools(tools=[AnswerQuestion]) first_responder = ResponderWithRetries( runnable=initial_chain, validator=PydanticToolsParser(tools=[AnswerQuestion]), ) if revisor is None: revision_chain = actor_prompt_template.partial( first_instruction=revise_instructions, function_name=ReviseAnswer.__name__, ) | model.bind_tools(tools=[ReviseAnswer]) revisor = ResponderWithRetries( runnable=revision_chain, validator=PydanticToolsParser(tools=[ReviseAnswer]), ) tool_node = tool_node or build_tool_node() workflow = StateGraph(State) workflow.add_node("draft", first_responder.respond) workflow.add_node("execute_tools", tool_node) workflow.add_node("revise", revisor.respond) workflow.add_edge(START, "draft") workflow.add_edge("draft", "execute_tools") workflow.add_edge("execute_tools", "revise") workflow.add_conditional_edges("revise", event_loop, ["execute_tools", END]) return workflow.compile(checkpointer=checkpointer or InMemorySaver())
和Reflection的build_app对比,区别一目了然:
- 多了
execute_tools节点 draft和revise绑定了不同的tool schema- 两者都包在
ResponderWithRetries里(带验证重试) - 条件路由从
should_continue换成了event_loop(双闸门)
五、两种模式的工程对比
把两种模式放在一起,从工程视角做一次全面对比:
| 对比维度 | Reflection | Reflexion |
|---|---|---|
| 核心思路 | 自我批评循环 | 自我批评 + 外部搜索验证 |
| LLM输出 | 纯文本(str) | 结构化(bind_tools + Pydantic) |
| 外部依赖 | 仅需LLM | LLM + 搜索工具(Tavily) |
| 消息角色 | 翻转(AI→Human) | 保持原始角色 |
| 终止策略 | 哨兵标记 + 硬上限 | 双闸门(MAX_QUERIES OR MAX_REVISIONS) |
| 容错 | 无重试 | ValidationError 重试(3轮) |
| 输出质量 | 依赖LLM自身能力 | 有引用、有搜索验证 |
| Provider兼容性 | 高(纯文本即可) | 中(需支持function calling) |
| 适用场景 | 创意写作、文本润色 | 知识问答、研究分析 |
选型建议很简单:如果你的场景不需要查外部资料,Reflection就够了,还省token;如果需要事实核查和引用支撑,上Reflexion。
六、要点总结
- Reflection的核心是"生成-批评-修订"的纯文本循环,通过角色翻转让LLM扮演师生关系,用哨兵标记<<NO_REVISIONS_NEEDED>>控制终止。
- Reflexion在Reflection基础上增加了结构化输出(bind_tools)、工具调用(Tavily搜索)和情景记忆,实现"开卷反思"。
- ResponderWithRetries的验证重试机制通过把ValidationError反馈给LLM来修复格式问题,最多重试3次。
学AI大模型的正确顺序,千万不要搞错了
🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!
有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!
就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋

📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇
学习路线:
✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经
以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!
我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~
这份完整版的大模型 AI 学习资料已经上传优快云,朋友们如果需要可以微信扫描下方优快云官方认证二维码免费领取【保证100%免费】

295



被折叠的 条评论
为什么被折叠?



