langgraph_practice.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. import os
  2. import operator
  3. from typing import Annotated, List, Tuple, TypedDict, Union, Literal
  4. from langchain_community.tools.tavily_search import TavilySearchResults
  5. from langchain_community.chat_models import ChatTongyi
  6. from langchain_core.prompts import ChatPromptTemplate
  7. from langchain.agents import create_agent
  8. from pydantic import BaseModel, Field
  9. #https://tavily.com/
  10. #uv pip install tavily-python -i https://pypi.tuna.tsinghua.edu.cn/simple
  11. # ---- 配置区:请替换为你自己的 Key ----
  12. os.environ["TAVILY_API_KEY"] = "your-tavily-key-here"
  13. DASHSCOPE_API_KEY = "your-dashscope-key-here"
  14. # 初始化搜索工具,max_results 控制每次搜索返回的条数
  15. search_tool = TavilySearchResults(max_results=3)
  16. tools = [search_tool]
  17. # 规划阶段用推理能力强的 Qwen-Max
  18. planner_llm = ChatTongyi(
  19. model="qwen-max",
  20. temperature=0.1, # 低温度保证规划稳定
  21. api_key=DASHSCOPE_API_KEY,
  22. )
  23. # 执行阶段可以用更经济的模型,这里用 Qwen-Plus
  24. executor_llm = ChatTongyi(
  25. model="qwen-plus",
  26. temperature=0.01,
  27. api_key=DASHSCOPE_API_KEY,
  28. )
  29. # 创建执行 Agent—— 它就是一个标准的 ReAct Agent
  30. executor_agent = create_agent(
  31. model=executor_llm,
  32. tools=tools,
  33. system_prompt="你是一个执行力很强的助手。请根据给定的任务步骤,准确完成并给出结果。",
  34. )
  35. class PlanExecute(TypedDict):
  36. """贯穿整个工作流的状态对象"""
  37. input: str # 用户原始输入
  38. plan: List[str] # 当前计划步骤列表
  39. # past_steps 用 operator.add 做累加——每次节点返回新步骤时自动追加到历史
  40. past_steps: Annotated[List[Tuple[str, str]], operator.add]
  41. response: str # 最终回复(非空时流程结束)
  42. class Plan(BaseModel):
  43. """规划器输出的结构化计划"""
  44. steps: List[str] = Field(
  45. description="需要顺序执行的步骤列表,每一步必须独立且包含完整上下文"
  46. )
  47. class Response(BaseModel):
  48. """直接回复用户的内容"""
  49. response: str
  50. class Act(BaseModel):
  51. """
  52. 重规划器的决策输出——要么回复用户,要么给出更新后的计划。
  53. 用 Union 类型确保一次只走一条分支。
  54. """
  55. action: Union[Response, Plan] = Field(
  56. description="如果任务已完成,返回 Response;如果还需要继续,返回 Plan"
  57. )
  58. # 规划器的系统提示——核心是"拆得合理、不越界"
  59. planner_prompt = ChatPromptTemplate.from_messages([
  60. (
  61. "system",
  62. """你是一个擅长任务分解的规划专家。对于用户提出的目标,请将其拆解为顺序执行的步骤列表。
  63. 规则:
  64. 1. 每一步必须自包含——执行者不需要上下文就能理解这步要做什么
  65. 2. 不要添加无关步骤,最后一步的结果应该直接导向最终答案
  66. 3. 优先使用中文描述步骤
  67. 4. 步骤数量控制在 3~6 步,过少说明拆分不到位,过多说明混入了无关操作""",
  68. ),
  69. ("placeholder", "{messages}"),
  70. ])
  71. # 将提示词模板和 LLM 拼成一条链,with_structured_output 让 LLM 直接输出 Plan 对象
  72. planner_chain = planner_prompt | planner_llm.with_structured_output(Plan)
  73. def plan_step(state: PlanExecute) -> dict:
  74. """规划节点:分析用户原始输入,生成执行计划"""
  75. result = planner_chain.invoke({"messages": [("user", state["input"])]})
  76. return {"plan": result.steps}
  77. result = planner_chain.invoke({
  78. "messages": [(
  79. "user",
  80. "帮我调研一下2025年国内主流代码助手工具(通义灵码、文心快码、CodeGeeX),"
  81. "从代码补全准确率、支持语言、价格三个维度做对比,最后给出选型建议。"
  82. )]
  83. })
  84. for i, step in enumerate(result.steps, 1):
  85. print(f" Step {i}: {step}")
  86. def execute_step(state: PlanExecute) -> dict:
  87. """
  88. 从当前计划中取出第一步,交给执行 Agent 去跑。
  89. 执行完后把 (步骤描述, 执行结果) 追加到 past_steps。
  90. """
  91. plan = state["plan"]
  92. # 把完整计划格式化,让 Agent 知道"我在做什么、后面还有什么"
  93. plan_overview = "\n".join(f" {i+1}. {step}" for i, step in enumerate(plan))
  94. current_task = plan[0] # 只取第一步
  95. # 拼装给 Agent 的指令:先展示全貌,再指定当前任务
  96. task_prompt = f"""以下是完整的执行计划:
  97. {plan_overview}
  98. 现在请你只执行第 1 步,不要做后续步骤:
  99. → {current_task}
  100. 请直接给出这一步的执行结果,不要啰嗦。"""
  101. agent_result = executor_agent.invoke({
  102. "messages": [("user", task_prompt)]
  103. })
  104. # 取 Agent 最后一条消息的内容作为这一步的产出
  105. step_output = agent_result["messages"][-1].content
  106. return {
  107. # 这一步走完后,计划需要弹出已执行的第一项——这个逻辑在 replan_step 里
  108. "past_steps": [(current_task, step_output)],
  109. }
  110. replanner_prompt = ChatPromptTemplate.from_template("""
  111. 你是一个项目进度管控专家。根据以下信息,判断当前任务的状态并做出决策。
  112. ## 用户原始目标
  113. {input}
  114. ## 原始计划
  115. {plan}
  116. ## 已完成的步骤及结果
  117. {past_steps}
  118. ## 决策规则
  119. 1. 如果所有步骤已完成且结果充分,直接给出最终答案(使用 Response)
  120. 2. 如果还有步骤未执行,返回更新后的计划(使用 Plan),注意:
  121. - 只保留尚未执行的步骤
  122. - 如果已完成步骤的结果表明原计划的后续步骤需要调整,直接修改
  123. - 不要返回已经做过的步骤
  124. 3. 如果某个步骤执行失败了,请在计划中补充重试或替代方案
  125. """)
  126. replanner_chain = replanner_prompt | planner_llm.with_structured_output(Act)
  127. def replan_step(state: PlanExecute) -> dict:
  128. """审视执行进度,决定下一步走向"""
  129. output = replanner_chain.invoke(state)
  130. if isinstance(output.action, Response):
  131. # 任务完成,直接返回给用户
  132. return {"response": output.action.response}
  133. else:
  134. # 还有步骤要执行,更新计划列表
  135. return {"plan": output.action.steps}
  136. from langgraph.graph import StateGraph, START, END
  137. # ---- 第一步:创建状态图,绑定状态类型 ----
  138. workflow = StateGraph(PlanExecute)
  139. # ---- 第二步:注册三个核心节点 ----
  140. workflow.add_node("planner", plan_step) # 规划节点
  141. workflow.add_node("executor", execute_step) # 执行节点
  142. workflow.add_node("replanner", replan_step) # 重规划节点
  143. # ---- 第三步:编排边的流向 ----
  144. workflow.add_edge(START, "planner") # 入口 → 规划
  145. workflow.add_edge("planner", "executor") # 规划 → 执行
  146. workflow.add_edge("executor", "replanner") # 执行 → 重规划
  147. # ---- 第四步:条件分支——重规划后走哪条路 ----
  148. def should_continue(state: PlanExecute) -> Literal["executor", "__end__"]:
  149. """如果 response 字段非空,说明重规划器认为任务完成,结束流程"""
  150. if state.get("response"):
  151. return END
  152. return "executor"
  153. workflow.add_conditional_edges("replanner", should_continue, {
  154. "executor": "executor",
  155. "__end__": END,
  156. })
  157. inputs = {
  158. "input": (
  159. "帮我调研2025年国内三大代码助手工具(通义灵码、文心快码、CodeGeeX),"
  160. "从代码补全准确率、支持语言、价格三个维度做对比,最后给出选型建议。"
  161. "请用中文输出最终结果。"
  162. )
  163. }
  164. # stream 模式可以看到每一步的中间输出
  165. for event in app.stream(inputs):
  166. for node_name, node_output in event.items():
  167. print(f"\n{'='*50}")
  168. print(f"📍 当前节点: {node_name}")
  169. # 避免打印太长的内容
  170. for key, value in node_output.items():
  171. if isinstance(value, str) and len(value) > 200:
  172. print(f" {key}: {value[:200]}...")
  173. else:
  174. print(f" {key}: {value}")