导语:在前两章,我们绘制了“旅小智”的宏伟蓝图,并搭建了其前后端的“骨骼”。现在,是时候为它注入“灵魂”了。本章将是一次从设计到实现、从后端到前端的完整开发实录,我们将把之前所有的理论和设想,全部转化为具体、可运行的代码。我们将深入
agents/目录,用 LangGraph 精心编排我们由“规划师”和多个“专家”组成的 AI 团队。然后,我们将这个强大的智能大脑接入 FastAPI 后端,替换掉之前的模拟函数。最后,我们还会对 Streamlit 前端进行优化,使其能更好地展示 Agent 生成的丰富内容。这是一个将所有知识点融会贯通的“大一统”章节,完成之后,你将拥有一个不再是“玩具”的、真正具备复杂问题解决能力的全栈 AI 应用。
目录
- 第一部分:智能体内核实现 (agents/)
- 工具准备:创建
agents/tools.py,定义(或模拟)city_search,hotel_search,flight_search等工具。 - 状态定义:创建
agents/state.py,实现我们在架构设计中构思的TripState。 - 专家 Agent 构建:创建
agents/experts.py,分别构建CityExpert,HotelExpert,FlightExpert的Runnable。 - 主管 Agent 构建:创建
agents/supervisor.py,实现核心的TravelPlannerAgent,它将作为图的中央路由器。 - 图的组装:创建
agents/graph.py,将所有节点和边连接起来,编译成最终的app。
- 工具准备:创建
- 第二部分:后端集成 (app/)
- 替换模拟函数:修改
app/main.py,导入并调用我们真实的agents.graph.app。 - 改造流式逻辑:调整
/invoke端点的stream_generator函数,使其能正确地处理并流式传输 LangGraph 返回的真实状态。
- 替换模拟函数:修改
- 第三部分:前端优化 (ui/)
- 美化与组件化:修改
ui/chat_app.py,使用st.container和st.expander来更结构化地展示信息。 - 解析结构化数据:当 Agent 返回的不仅仅是文本(例如,一个酒店列表的 JSON),前端需要能解析并以更友好的方式(如卡片、表格)展示。
- 增加交互组件:添加“新会话”按钮,允许用户清空历史,开启一次全新的旅行规划。
- 美化与组件化:修改
- 终极联调:见证“旅小智”的诞生
- 启动完整的全栈应用。
- 进行一次完整的、从模糊需求到具体行程规划的多轮对话测试。
- 总结:从组件到系统的“涌现”
1. 第一部分:智能体内核实现 (agents/)
这是我们项目的核心。我们将逐一创建所需的文件。
agents/tools.py:工具准备
为简化项目,我们使用模拟工具,它们只返回固定的假数据。
# agents/tools.py
from langchain_core.tools import tool
@tool
def search_city_info(city: str):
"""Searches for information about a given city, like attractions, food, etc."""
print(f"--- Searching info for {city} ---")
if city.lower() == "厦门":
return "厦门是一个美丽的海滨城市,有鼓浪屿、南普陀寺等著名景点,海鲜美食非常出名。"
return f"没有找到关于 {city} 的信息。"
@tool
def search_hotels(city: str, budget: str):
"""Searches for hotels in a given city based on budget."""
print(f"--- Searching hotels in {city} with budget {budget} ---")
return [
{"name": "厦门海景大酒店", "price": "1200元/晚", "rating": 4.8},
{"name": "鼓浪屿花园民宿", "price": "600元/晚", "rating": 4.9},
]
# ... 可以添加更多的模拟工具,如 search_flights ...
agents/state.py:状态定义
# agents/state.py
from typing import TypedDict, Annotated, List, Optional, Dict, Any
from langchain_core.messages import BaseMessage
import operator
class TripState(TypedDict):
user_request: Optional[str] = None
trip_plan: Optional[Dict[str, Any]] = None
expert_outputs: Annotated[dict, operator.add] = {}
final_itinerary: Optional[str] = None
messages: Annotated[List[BaseMessage], operator.add]
next_node: str
我们定义了一个比之前更丰富的 TripState,它能清晰地追踪规划过程中的各种结构化数据。
agents/experts.py:专家 Agent 构建
每个专家都是一个简单的、只包含一个工具的 ReAct Agent。
# agents/experts.py
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain.agents import create_openai_tools_agent, AgentExecutor
from .tools import search_city_info, search_hotels
def create_expert_agent(tools: list, system_prompt: str):
llm = ChatOpenAI(model="deepseek-chat")
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("user", "{input}")
])
agent = create_openai_tools_agent(llm, tools, prompt)
return AgentExecutor(agent=agent, tools=tools, verbose=True)
# 创建城市专家
city_expert_runnable = create_expert_agent(
tools=[search_city_info],
system_prompt="You are a city expert. Your job is to provide detailed information about a city."
)
# 创建酒店专家
hotel_expert_runnable = create_expert_agent(
tools=[search_hotels],
system_prompt="You are a hotel expert. Your job is to find suitable hotels based on user's criteria."
)
agents/supervisor.py:主管 Agent 构建
主管是核心路由器,它使用 Function Calling 来决定下一步该调用哪个专家。
# agents/supervisor.py
from langchain_core.pydantic_v1 import BaseModel, Field
from typing import Literal
class Route(BaseModel):
"""Decide the next step to take."""
destination: Literal["CityExpert", "HotelExpert", "FinalItinerary", "End"] = Field(
description="The next expert agent to call, or 'FinalItinerary' to generate the final plan, or 'End' to finish."
)
def create_supervisor_runnable():
llm = ChatOpenAI(model="gpt-4-turbo")
structured_llm = llm.with_structured_output(Route)
prompt = ChatPromptTemplate.from_messages([
("system", """You are the TravelPlanner, a supervisor of a team of AI experts.
Based on the user's request and the conversation history, decide the next expert to call.
Your available experts are: CityExpert, HotelExpert.
- If the user is asking about city information, call CityExpert.
- If the user is asking about hotels, call HotelExpert.
- When all information is gathered, call FinalItinerary.
- If the user is satisfied or says goodbye, call End."""),
("user", "{input}\n\nConversation History:\n{history}")
])
return prompt | structured_llm
supervisor_runnable = create_supervisor_runnable()
agents/graph.py:图的组装
现在,我们将所有部分组装起来。
# agents/graph.py
import functools
from langgraph.graph import StatefulGraph, END
from .state import TripState
from .supervisor import supervisor_runnable
from .experts import city_expert_runnable, hotel_expert_runnable
# --- 定义节点函数 ---
def supervisor_node(state: TripState):
print("--- Calling Supervisor ---")
# 为了简化,我们假设 history 是从 state['messages'] 中提取的
route = supervisor_runnable.invoke({
"input": state['messages'][-1].content,
"history": ""
})
return {"next_node": route.destination}
def expert_node(state: TripState, expert_runnable, expert_name: str):
print(f"--- Calling {expert_name} ---")
result = expert_runnable.invoke({"input": state['messages'][-1].content})
return {"expert_outputs": {expert_name: result['output']}}
def final_itinerary_node(state: TripState):
print("--- Generating Final Itinerary ---")
# 在真实应用中,这里会调用一个 LLM 来汇总所有信息
final_report = f"Your trip plan based on gathered info: {state['expert_outputs']}"
return {"final_itinerary": final_report}
# --- 组装图 ---
workflow = StatefulGraph(TripState)
workflow.add_node("Supervisor", supervisor_node)
workflow.add_node("CityExpert", functools.partial(expert_node, expert_runnable=city_expert_runnable, expert_name="CityExpert"))
workflow.add_node("HotelExpert", functools.partial(expert_node, expert_runnable=hotel_expert_runnable, expert_name="HotelExpert"))
workflow.add_node("FinalItinerary", final_itinerary_node)
# 定义路由逻辑
def router(state: TripState):
return state['next_node']
workflow.add_conditional_edges(
"Supervisor",
router,
{
"CityExpert": "CityExpert",
"HotelExpert": "HotelExpert",
"FinalItinerary": "FinalItinerary",
"End": END
}
)
# 所有专家节点执行完后,都回到主管进行下一步决策
workflow.add_edge("CityExpert", "Supervisor")
workflow.add_edge("HotelExpert", "Supervisor")
workflow.add_edge("FinalItinerary", END)
workflow.set_entry_point("Supervisor")
# 接入持久化
from langgraph.checkpoint.sqlite import SqliteSaver
memory_saver = SqliteSaver.from_conn_string(":memory:") # 使用内存数据库进行测试
app = workflow.compile(checkpointer=memory_saver)
我们的智能体内核现在完成了!它是一个功能齐全、由主管驱动、专家协作的多智能体系统。
2. 第二部分:后端集成 (app/)
现在,我们将这个真实的 app 接入 FastAPI。
修改 app/main.py
# app/main.py
# ... (保留 FastAPI 和 CORS 的设置) ...
# 1. 导入真实的 Agent App
from agents.graph import app as agent_app
# ... (保留 InvokeRequest 模型) ...
# 2. 改造流式响应逻辑
@app.post("/invoke")
async def invoke_agent(request: InvokeRequest):
thread_id = request.thread_id or str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}
async def stream_generator():
# 准备 Agent 的输入
inputs = {"messages": [("user", request.input)]}
# 使用 astream_events 来获取更详细的事件流
# v1 版本的 astream 返回的是状态快照
async for event in agent_app.astream_events(inputs, config, version="v1"):
kind = event["event"]
if kind == "on_chain_end":
# 当 Supervisor 或某个 Expert 完成一轮工作时
if event["name"] in ["Supervisor", "CityExpert", "HotelExpert"]:
yield {
"event": "message",
"data": f"**{event['name']}** is thinking...",
"id": thread_id
}
if kind == "on_tool_end":
# 当一个工具被调用结束时
yield {
"event": "message",
"data": f"\n\n> Called tool: `{event['name']}` with output:\n{event['data'].get('output')}\n",
"id": thread_id
}
if kind == "on_chain_end" and event["name"] == "FinalItinerary":
# 当最终结果生成时
final_report = event['data']['output'].get('final_itinerary')
if final_report:
yield {
"event": "final_result",
"data": final_report,
"id": thread_id
}
return EventSourceResponse(stream_generator())
我们使用了 app.astream_events,这是一个更底层的流式接口,它能让我们监听到图执行过程中的各种事件(如节点开始/结束,工具开始/结束),从而可以向前端发送更丰富、更具语义的实时信息。
3. 第三部分:前端优化 (ui/)
修改 ui/chat_app.py
# ui/chat_app.py (部分修改)
# ... (保留大部分已有代码) ...
# --- 新增一个“新会话”按钮 ---
if st.button("开启新会话"):
st.session_state.messages = []
st.session_state.thread_id = None
st.rerun() # 重新运行脚本以刷新界面
# ... (接收用户输入的逻辑不变) ...
# --- 调用后端 API (改造解析逻辑) ---
with st.chat_message("assistant"):
message_placeholder = st.empty()
full_response = ""
# ... (requests 部分不变) ...
for chunk in r.iter_content(chunk_size=None):
if chunk:
chunk_str = chunk.decode('utf-8')
if chunk_str.startswith("data:"):
data_str = chunk_str[len("data:"):].strip()
try:
data_json = json.loads(data_str)
# 更新 thread_id
if 'id' in data_json and st.session_state.thread_id is None:
st.session_state.thread_id = data_json['id']
# 根据事件类型来更新 UI
event_type = data_json.get("event")
if event_type == "message":
full_response += data_json['data']
message_placeholder.markdown(full_response + "▌")
elif event_type == "final_result":
full_response = data_json['data']
# 可以用更丰富的形式展示最终结果
message_placeholder.markdown("### 您的专属行程单")
st.markdown(full_response)
except json.JSONDecodeError:
pass
# 最终的完整回复应该是行程单
message_placeholder.markdown(full_response)
前端的改动相对较小,主要是调整了对后端返回的 SSE 事件的解析逻辑,以便能分别处理中间消息和最终结果。
4. 终极联调:见证“旅小智”的诞生
现在,按照 3.2 节中的步骤,分别启动后端和前端服务。
- 启动后端:
cd app && uvicorn main:app --port 8000 - 启动前端:
cd ui && streamlit run chat_app.py
打开浏览器,开始一场完整的对话:
你: "你好,我想去厦门玩。"
AI (流式显示): Supervisor is thinking...
CityExpert is thinking...
Called tool:
search_city_infowith output: 厦门是一个美丽的海滨城市...好的,厦门是个不错的选择!为了帮您更好地规划,您对酒店有什么预算要求吗?
你: "预算大概每晚 1000 元以内吧"
AI (流式显示): Supervisor is thinking...
HotelExpert is thinking...
Called tool:
search_hotelswith output: [{'name': '厦门海景...', 'price': '1200元/晚'}, {'name': '鼓浪屿花园...', 'price': '600元/晚'}]... (Agent 会继续对话,直到生成最终行程)
你将亲眼见证一个由多个 AI 智能体组成的团队,在你的指令下,分工协作、实时思考、并最终完成任务的全过程。
5. 总结:从组件到系统的“涌现”
本章是整个第三周课程的顶点。我们没有学习太多全新的理论,而是将之前所有独立的知识点——FastAPI, Streamlit, LangGraph, 多智能体架构——全部整合在了一起。
你看到了一个复杂的系统是如何从一个个简单的组件“涌现”出来的。你也体会到了,一个真正的 AI 应用,其工程复杂度远不止于算法本身,还包含了大量的服务集成、API 设计、状态管理和前端交互工作。
完成了“旅小智”的开发,你已经具备了设计和实现一个端到端、准生产级 AI 应用的完整能力。你不再只是一个“调包侠”,而是一个懂得如何将 AI 的智慧,通过扎实的软件工程,转化为可靠、好用的产品的“AI 全栈工程师”。