Agent 从小白到架构师
← 返回教程总览
第五课

构建完整的旅行规划助手应用

从 LangChain 入门到带记忆的 Web 旅行规划助手

前四课已经完成了核心能力。现在把它打包成一个完整的 Web 应用。

1. 最终架构

```text ┌──────────────────────────┐ │ Web 界面 │ │ HTML + CSS + JS │ └──────────────┬───────────┘ │ HTTP 请求 ▼ ┌──────────────────────────┐ │ Flask 服务器 │ │ Python 后端 │ └──────────────┬───────────┘ │ 调用 ▼ ┌──────────────────────────┐ │ LangChain Agent │ │ 工具 + LLM + 记忆 │ └──────────────────────────┘ ```

2. 项目结构

```text lesson05/ ├── app.py ├── agent.py ├── templates/ │ └── index.html ├── static/ │ ├── style.css │ ├── app_demo.png │ └── chat_demo.png ├── run.sh └── requirements.txt ```

3. Agent 模块

```python import os import base64 import hashlib import hmac import time from datetime import datetime from urllib import parse

import requests from langchain_openai import ChatOpenAI from langchain_core.tools import tool from langchain.agents import create_agent from langgraph.checkpoint.memory import InMemorySaver

SENIVERSE_UID = os.getenv("SENIVERSE_UID", "") SENIVERSE_KEY = os.getenv("SENIVERSE_KEY", "") SENIVERSE_API_URL = "https://api.seniverse.com/v3/weather/now.json"

@tool def get_weather(city: str) -> str: """查询城市实时天气""" try: ts = int(time.time()) params_str = f"ts={ts}&uid={SENIVERSE_UID}" key = bytes(SENIVERSE_KEY, "UTF-8") raw = bytes(params_str, "UTF-8") digester = hmac.new(key, raw, hashlib.sha1).digest() sig = parse.quote(base64.b64encode(digester).decode("utf8")) url = ( f"{SENIVERSE_API_URL}?location={parse.quote(city)}" f"&{params_str}&sig={sig}" )

    data = requests.get(url, timeout=5).json()
    if "results" not in data:
        return f"未找到 {city} 的天气信息"

    result = data["results"][0]
    now = result["now"]
    return f"{result['location']['name']}:{now['text']},温度 {now['temperature']}°C"
except Exception as e:
    return f"获取天气失败: {str(e)}"

@tool def get_current_time() -> str: """获取当前时间""" return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

@tool def search_attractions(city: str) -> str: """搜索城市的热门景点""" attractions = { "北京": "故宫、长城、颐和园、天坛、南锣鼓巷", "上海": "外滩、东方明珠、豫园、南京路、迪士尼", "杭州": "西湖、灵隐寺、宋城、西溪湿地、千岛湖", "成都": "宽窄巷子、锦里、大熊猫基地、都江堰、青城山", } return attractions.get(city, f"{city} 暂无景点信息")

@tool def estimate_travel_time(from_city: str, to_city: str) -> str: """估算两个城市之间的交通时间""" times = { ("北京", "上海"): "高铁约 4.5 小时,飞机约 2 小时", ("上海", "杭州"): "高铁约 1 小时,自驾约 2 小时", ("北京", "杭州"): "高铁约 5 小时,飞机约 2 小时", ("成都", "北京"): "高铁约 8 小时,飞机约 2.5 小时", } key = (from_city, to_city) reverse_key = (to_city, from_city) return times.get(key) or times.get(reverse_key) or f"{from_city} 到 {to_city} 的交通信息暂无"

def create_travel_agent(): llm = ChatOpenAI( temperature=0.7, model="glm-4.7", openai_api_key=os.getenv("ZHIPU_API_KEY"), openai_api_base="https://open.bigmodel.cn/api/paas/v4/", )

memory = InMemorySaver()
tools = [get_weather, get_current_time, search_attractions, estimate_travel_time]
return create_agent(llm, tools, checkpointer=memory)

```

4. Flask 后端

```python import uuid from flask import Flask, jsonify, render_template, request

from agent import create_travel_agent

app = Flask(name) agent = create_travel_agent()

@app.route("/") def index(): return render_template("index.html")

@app.route("/chat", methods=["POST"]) def chat(): try: data = request.json or {} user_message = data.get("message", "") thread_id = data.get("thread_id", str(uuid.uuid4()))

    if not user_message:
        return jsonify({"error": "消息不能为空"}), 400

    config = {"configurable": {"thread_id": thread_id}}
    result = agent.invoke(
        {"messages": [{"role": "user", "content": user_message}]},
        config,
    )

    tool_calls = []
    for msg in result["messages"]:
        if type(msg).__name__ == "ToolMessage":
            tool_calls.append({
                "tool": msg.name,
                "result": msg.content[:100] + "..." if len(msg.content) > 100 else msg.content,
            })

    return jsonify({
        "response": result["messages"][-1].content,
        "thread_id": thread_id,
        "tool_calls": tool_calls,
    })
except Exception as e:
    return jsonify({"error": str(e)}), 500

@app.route("/new_chat", methods=["POST"]) def new_chat(): return jsonify({"thread_id": str(uuid.uuid4())})

if name == "main": app.run(debug=True, port=8080, host="0.0.0.0") ```

5. 前端页面

前端可以保持简单:一个消息列表、一个输入框、一个发送按钮、一个工具调用记录区域。

```html

旅行规划助手

基于 LangChain Agent 的智能旅行规划

<div class="chat-container">
  <div class="messages" id="messages"></div>
  <div class="input-area">
    <input id="user-input" placeholder="例如:帮我规划一个北京到上海的三日游" />
    <button id="send-btn">发送</button>
  </div>
</div>
\`\`\`

6. 启动方式

```sh cd course/lesson05 ./run.sh ```

然后访问:

```text http://localhost:8080 ```

7. 最终功能清单

  • 实时天气查询
  • 城市景点推荐
  • 出行交通时间估算
  • 多轮记忆对话
  • 工具调用过程可视化
  • Web 端交互体验

完整项目建议

如果你要把这套教程继续往下扩展,可以按这个顺序演进:

  1. 把天气、交通、酒店都改成真实 API
  2. 增加预算约束,比如“3000 元以内”
  3. 增加日期规划,比如“生成 3 天详细行程”
  4. 把 `InMemorySaver` 替换成数据库持久化
  5. 给前端加用户会话列表和历史记录

学完后你应该掌握什么

  • 能独立创建 LangChain Agent
  • 能给 Agent 添加真实世界工具
  • 能实现多轮记忆对话
  • 能把复杂需求拆成多工具工作流
  • 能把 Agent 封装成完整 Web 应用

这个教程和 Claude Code 那条路线不一样。前者偏 Agent 产品开发,后者偏 Coding Agent 架构拆解。放在同一个站点里,正好能形成两条互补的学习路径。