3.3 拒绝“玩具”!“旅小智”项目全栈开发实录

1 阅读1分钟

导语:在前两章,我们绘制了“旅小智”的宏伟蓝图,并搭建了其前后端的“骨骼”。现在,是时候为它注入“灵魂”了。本章将是一次从设计到实现、从后端到前端的完整开发实录,我们将把之前所有的理论和设想,全部转化为具体、可运行的代码。我们将深入 agents/ 目录,用 LangGraph 精心编排我们由“规划师”和多个“专家”组成的 AI 团队。然后,我们将这个强大的智能大脑接入 FastAPI 后端,替换掉之前的模拟函数。最后,我们还会对 Streamlit 前端进行优化,使其能更好地展示 Agent 生成的丰富内容。这是一个将所有知识点融会贯通的“大一统”章节,完成之后,你将拥有一个不再是“玩具”的、真正具备复杂问题解决能力的全栈 AI 应用。

目录

  1. 第一部分:智能体内核实现 (agents/)
    • 工具准备:创建 agents/tools.py,定义(或模拟)city_search, hotel_search, flight_search 等工具。
    • 状态定义:创建 agents/state.py,实现我们在架构设计中构思的 TripState
    • 专家 Agent 构建:创建 agents/experts.py,分别构建 CityExpert, HotelExpert, FlightExpertRunnable
    • 主管 Agent 构建:创建 agents/supervisor.py,实现核心的 TravelPlanner Agent,它将作为图的中央路由器。
    • 图的组装:创建 agents/graph.py,将所有节点和边连接起来,编译成最终的 app
  2. 第二部分:后端集成 (app/)
    • 替换模拟函数:修改 app/main.py,导入并调用我们真实的 agents.graph.app
    • 改造流式逻辑:调整 /invoke 端点的 stream_generator 函数,使其能正确地处理并流式传输 LangGraph 返回的真实状态。
  3. 第三部分:前端优化 (ui/)
    • 美化与组件化:修改 ui/chat_app.py,使用 st.containerst.expander 来更结构化地展示信息。
    • 解析结构化数据:当 Agent 返回的不仅仅是文本(例如,一个酒店列表的 JSON),前端需要能解析并以更友好的方式(如卡片、表格)展示。
    • 增加交互组件:添加“新会话”按钮,允许用户清空历史,开启一次全新的旅行规划。
  4. 终极联调:见证“旅小智”的诞生
    • 启动完整的全栈应用。
    • 进行一次完整的、从模糊需求到具体行程规划的多轮对话测试。
  5. 总结:从组件到系统的“涌现”

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 节中的步骤,分别启动后端和前端服务。

  1. 启动后端: cd app && uvicorn main:app --port 8000
  2. 启动前端: cd ui && streamlit run chat_app.py

打开浏览器,开始一场完整的对话:

: "你好,我想去厦门玩。"

AI (流式显示): Supervisor is thinking...

CityExpert is thinking...

Called tool: search_city_info with output: 厦门是一个美丽的海滨城市...

好的,厦门是个不错的选择!为了帮您更好地规划,您对酒店有什么预算要求吗?

: "预算大概每晚 1000 元以内吧"

AI (流式显示): Supervisor is thinking...

HotelExpert is thinking...

Called tool: search_hotels with output: [{'name': '厦门海景...', 'price': '1200元/晚'}, {'name': '鼓浪屿花园...', 'price': '600元/晚'}]

... (Agent 会继续对话,直到生成最终行程)

你将亲眼见证一个由多个 AI 智能体组成的团队,在你的指令下,分工协作、实时思考、并最终完成任务的全过程。

5. 总结:从组件到系统的“涌现”

本章是整个第三周课程的顶点。我们没有学习太多全新的理论,而是将之前所有独立的知识点——FastAPI, Streamlit, LangGraph, 多智能体架构——全部整合在了一起。

你看到了一个复杂的系统是如何从一个个简单的组件“涌现”出来的。你也体会到了,一个真正的 AI 应用,其工程复杂度远不止于算法本身,还包含了大量的服务集成、API 设计、状态管理和前端交互工作。

完成了“旅小智”的开发,你已经具备了设计和实现一个端到端、准生产级 AI 应用的完整能力。你不再只是一个“调包侠”,而是一个懂得如何将 AI 的智慧,通过扎实的软件工程,转化为可靠、好用的产品的“AI 全栈工程师”。