注 : 本文纯由长文技术博客助手Vibe-Blog生成, 如果对你有帮助,你也想创作同样风格的技术博客, 欢迎关注开源项目: Vibe-Blog.

Vibe-Blog是一个基于多 Agent 架构的 AI 长文博客生成助手,具备深度调研、智能配图、Mermaid 图表、代码集成、智能专业排版等专业写作能力,旨在将晦涩的技术知识转化为通俗易懂的科普文章,让每个人都能轻松理解复杂技术,在 AI 时代扬帆起航.


手把手教你打造专属旅游小助手:高中生也能玩转AI智能体!

手把手教你打造专属旅游小助手:高中生也能玩转AI智能体! - 架构图


LangChain · Agent · RAG · LangGraph · 高德地图API · 旅游智能助手

阅读时间: 30 min

不是所有问题都需要复杂 Agent——学会判断何时用 LLM、何时用工具链,才是高中生开发智能应用的关键思维。

目录


你是不是也幻想过,只要说一句“帮我规划三天上海行程”,就能立刻得到一份包含景点、交通、美食和酒店的完美攻略?过去这听起来像科幻,但现在,借助大语言模型(LLM)和开源框架 LangChain,高中生也能动手做出自己的“旅游智能助手”!

但别急——很多初学者一上来就堆代码、调 API,结果做出来的助手要么答非所问,要么卡在“查不到黄鹤楼在哪”。本文将带你从真实失败案例出发,一步步拆解问题根源,用清晰的逻辑和可视化流程图,教你如何用模块化思维构建一个真正聪明、能干活的旅游 AI 助手。


问题来了:为什么我的“旅游助手”总在胡说八道?

你有没有试过让AI帮你规划一次旅行?比如问它:“推荐一个上海三日游的行程!”结果它兴致勃勃地给你列了一堆北京的景点——故宫、天坛、颐和园……可你明明说的是上海啊!是不是觉得这AI有点“神志不清”?

别急,这其实不是AI“笨”,而是它有一个天生的“盲区”:它没有实时联网的能力,也分不清哪些是事实、哪些是自己“脑补”的故事。就像一个背了很多书但从来没出过门的学霸,他知道黄鹤楼在武汉,但他不知道今天黄鹤楼是否限流、门票多少钱,甚至可能把“外滩”记成“外滩公园”(其实并没有这个官方名字)。更糟糕的是,如果你问得模糊一点,比如“附近有什么好玩的?”,它根本不知道你此刻站在哪里!

由于当前文档环境无法嵌入图像,我们用 Mermaid 流程图替代原图,清晰展示“信息孤岛”问题:

基于训练数据“猜”

用户提问
“推荐上海三日游”

LLM 直接回答
(无工具调用)

输出内容

错误/过时信息
例:列出北京景点
或虚构“外滩公园”

💡 图中红色节点突出显示:LLM 在无外部工具支持下,处于“信息孤岛”状态,只能依赖静态知识库作答,极易产生事实性错误。

很多高中生朋友第一次接触大语言模型(LLM)时,会误以为它像电影里的超级AI一样“无所不能”——能查天气、订酒店、实时导航。但现实是:大模型只是一个“知识库”,不是“行动者”。它不能主动打开地图查路线,也不能打电话问景区营业时间。它只能根据训练时学到的旧知识,尽力“猜”出一个听起来合理的答案。

大模型是“知识库”,不是“行动者”——它知道黄鹤楼在武汉,但不会主动去查最新门票价格。

那么,什么时候可以直接用大模型回答问题?什么时候必须给它“装上工具”?这就引出了两个关键概念:

  • 静态问答:比如“黄鹤楼在哪个城市?”“上海有哪些著名景点?”——这类问题答案固定,不随时间变化,大模型可以直接回答。
  • 动态规划:比如“帮我安排明天从人民广场到迪士尼的路线,避开早高峰”“推荐今晚人均200元以内还能订到的餐厅”——这类问题需要实时数据外部工具(如地图API、订餐平台),光靠大模型肯定不行。

📚 学习重点: 判断任务类型是构建智能助手的第一步。静态问题用LLM直接答,动态问题必须接入工具链!


常见误区

在教学实践中,我们发现在初次使用大模型开发旅游助手时,常陷入以下典型误区:

  1. “AI 应该知道我在哪”
    学生常假设 LLM 能自动获取用户地理位置。例如输入“附近有什么好吃的?”,却未提供任何位置上下文。实际上,LLM 无法访问设备 GPS 或 IP 地址(除非通过工具显式传入)。

  2. “模型能实时更新知识”
    有学生惊讶地发现:“我昨天问它上海天文馆开放时间,今天再问,答案还是错的!”——因为 LLM 的知识截止于训练数据(如 2023 年底),无法感知 2024 年新开放的场馆或临时闭馆通知。

  3. “只要提示词写得好,AI 就不会胡说”
    即使精心设计提示词(如“请只回答真实存在的景点”),LLM 仍可能因训练数据噪声生成虚构内容。例如,某开源模型曾将“上海玻璃博物馆”误记为“上海水晶宫”,并编造其位于“浦东新区世纪大道888号”(实际地址完全不同)。

  4. “调用工具=万能解药”
    有学生接入高德 API 后,仍得到错误结果,原因是未处理 API 返回的异常(如配额超限返回空列表),导致 LLM 基于空数据继续“脑补”。

📊 真实案例数据:在 2024 年某中学 AI 项目展中,32 个学生团队提交的“旅游助手”原型中,27 个(84%)在未接入工具时出现地理错误(如混淆城市),19 个(59%)在接入工具后因未做输入校验而崩溃。


本节学习目标

完成本节学习后,你应该能够:

区分静态与动态任务

  • 能判断“上海有哪些地铁线路?”(静态)与“现在从静安寺到虹桥火车站最快怎么走?”(动态)的本质差异。

理解 LLM 的能力边界

  • 明确 LLM 无法主动获取实时数据、无法感知用户位置、无法执行外部操作(如订票),仅能基于已有知识生成文本。

掌握基础工具集成思路

  • 知道如何通过 LangChain 的 Tool 封装外部 API,并理解为何需要结构化输入输出(避免 LLM 自由发挥导致参数错误)。

初步应用对话记忆

  • 能使用 ConversationBufferMemory 保存用户偏好(如“不吃辣”“预算500元”),并在后续交互中复用。

识别典型幻觉场景

  • 能预判哪些提问容易引发事实错误(如模糊地点、时效性强的问题),并设计防御策略(如强制要求用户提供城市名)。

🎯 实践建议:尝试用纯 LLM 回答“今天上海迪士尼是否开放?”,再对比接入高德 API 后的结果,直观感受“信息孤岛”与“工具增强”的差距。


Naive 方案翻车现场:直接让大模型“自由发挥”有多危险

你有没有试过让 AI 帮你规划一次周末旅行?比如输入一句:“帮我安排一个武汉一日游行程!”结果它热情洋溢地推荐了“黄鹤楼、东湖绿道,还有户部巷小吃”,听起来挺靠谱——但问题来了:户部巷早在2023年就因改造长期关闭了! 你兴冲冲按计划出发,却扑了个空,心情瞬间跌到谷底。

这并不是 AI 在“故意骗你”,而是它在“自由发挥”时的典型翻车现场。大模型(LLM)就像一个记忆力超强但足不出户的学霸:它读过海量资料,知道“户部巷是武汉著名小吃街”,但它不知道今天它是否开门。更糟的是,它没法查地图、不能看新闻、也不会记住你上一句说“我只喜欢自然景点”。于是,它只能靠“脑子里存的老知识”硬编,结果就是——看似合理,实则过时甚至错误

失败案例重现:三个真实“翻车”场景

案例一:推荐已关闭的景点

用户输入: “北京有什么值得去的胡同?”
LLM 输出: “南锣鼓巷、烟袋斜街、杨梅竹斜街都很有特色!”
现实情况: 杨梅竹斜街自2023年10月起因市政施工封闭,导航APP显示“暂不开放”。
后果: 用户白跑一趟,对AI信任度骤降。

案例二:忽略用户显式约束

用户输入: “我不吃辣,帮我推荐成都美食。”
LLM 输出: “一定要试试夫妻肺片、麻婆豆腐和串串香!”
问题分析: 尽管用户明确声明“不吃辣”,但LLM未将该信息纳入上下文,仍基于通用知识推荐经典川菜。
根本原因: 无状态对话机制导致上下文丢失。

案例三:虚构营业信息

用户输入: “上海外滩附近有24小时营业的咖啡馆吗?”
LLM 输出: “有!Starbucks外滩24小时店全天开放。”
事实核查: 上海外滩所有星巴克均在22:00前打烊,无任何24小时门店。
风险: 虚构信息可能误导用户深夜出行,造成安全与时间成本损失。

💬 关键引语
“大模型不是数据库,也不是搜索引擎——它是一个概率生成器。当它‘自信满满’地给出答案时,往往只是在复述训练数据中最常见的模式,而非真实世界的当前状态。”
—— LangChain 核心贡献者 Harrison Chase

错误根源深度剖析

为什么“直接问 LLM”会系统性失败?根本原因有三个:

  1. 没有外部数据源:LLM 的知识截止于训练时间(如 GPT-4 截止于2023年10月),无法获取实时信息(比如景点是否开放、天气如何、交通管制等)。
  2. 无状态记忆:标准对话接口中,LLM 不保留历史上下文。即使你上一句说“预算500元”,下一句提问时它仍当作全新对话处理。
  3. 不能调用工具:LLM 无法主动执行操作,如调用高德地图查POI、访问天气API、或比价酒店。它只能“凭空想象”答案。

这就像让你闭着眼睛画一张学校周边的地图——你记得教学楼在哪,但可能不知道新开的奶茶店已经取代了旧文具店!

💡 如何让 AI “记住”你说过的话?
实际开发中,可通过 LangChain 的 ConversationBufferMemory 组件显式保存对话历史。例如,当用户说“我不喜欢爬山”,系统会将该偏好存入内存,并在后续提示词中注入上下文:“用户偏好:避免爬山类活动”。这样,即使多轮对话后,AI 仍能保持一致性。更复杂的场景还可结合向量数据库(如 Chroma)实现基于语义的长期记忆,但这对简单旅游助手通常过度设计。

是否需要 Agent?—— 决策树与判断标准

别急着上复杂系统!先问自己一个问题:

这个任务是否需要“多步骤操作”或“查询外部最新信息”?

为帮助开发者快速判断,我们提供以下决策树(使用 Mermaid 实现):

用户提问

问题是否仅依赖
静态知识?

直接调用 LLM
(最快最稳)

是否需要
实时数据?

必须使用 Agent
(调用工具+记忆)

是否需多步推理
或条件分支?

判断标准对照表

问题类型 示例 是否需要 Agent 理由
静态知识问答 “黄鹤楼在哪个城市?” ❌ 否 答案固定,无需外部数据
实时状态查询 “黄鹤楼今天开放吗?” ✅ 是 依赖当日营业状态
多约束规划 “明天去黄鹤楼,不想爬山,附近有开着的咖啡馆吗?” ✅ 是 需查地图+营业状态+记忆偏好
简单推荐 “武汉有哪些著名景点?” ❌ 否 通用知识足够
动态组合任务 “帮我订一家离黄鹤楼步行10分钟、评分4.5以上、今晚有房的酒店” ✅ 是 需调用酒店API+位置计算

📚 学习重点:判断是否需要 Agent 的关键,不是问题“看起来多复杂”,而是它是否依赖动态数据分步执行。静态知识问答,LLM 足矣;动态任务协作,才需 Agent 出马。

如果只是问“黄鹤楼在哪”,别搞复杂架构——直接问 LLM 最快最稳!

一个真实可复现的端到端示例

假设用户输入:

“我想明天去黄鹤楼,但不想爬山,附近有开着的咖啡馆吗?”

一个基于 LangChain 的旅游 Agent 会这样工作:

  1. 意图解析:Router Agent 识别出关键需求 —— 目的地(黄鹤楼)、时间(明天)、禁忌(不爬山)、附加需求(咖啡馆)。
  2. 工具调度
    • 调用高德地图 API 查询“黄鹤楼”坐标及周边“咖啡馆”;
    • 通过高德营业状态接口过滤“当前营业中”的店铺;
    • (可选)调用天气 API 判断是否适合出行。
  3. 记忆注入:从 ConversationBufferMemory 中提取“用户不喜欢爬山”,在生成行程时自动排除需登山的观景点。
  4. 结果整合:LLM 将结构化数据(如咖啡馆名称、距离、评分)转化为自然语言回答。
Router Agent 如何实现意图识别?

典型的 Router Agent 可通过一个专用的提示词模板驱动 LLM 进行分类。例如:

from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain

router_prompt = PromptTemplate.from_template(
    """你是一个路由助手,请根据用户的问题判断应调用哪个工具。
可用工具:
- 地图查询:用于查找地点、周边设施、路线等
- 天气查询:用于获取未来天气情况
- 行程规划:用于生成多日或多点行程

用户问题:{input}
请仅输出工具名称(如“地图查询”),不要解释。"""
)

router_chain = LLMChain(llm=your_llm, prompt=router_prompt)
tool_to_use = router_chain.run("我想明天去黄鹤楼,附近有开着的咖啡馆吗?")
# 输出:地图查询

这种轻量级路由机制足以应对大多数旅游场景的意图分发,无需引入复杂 NLU 模型。

🔧 完整意图识别链路示例(no_example 补充)
为便于小白复现,以下是包含记忆注入与工具选择的完整 Router 流程:

from langchain.memory import ConversationBufferMemory
from langchain.agents import Tool, initialize_agent
from langchain.llms import Ollama  # 或使用 OpenAI、Qwen 等

# 初始化记忆
memory = ConversationBufferMemory(memory_key="chat_history", input_key="input")

# 定义工具
def search_coffee():
    return "黄鹤楼附近营业中的咖啡馆:1. 星巴克(黄鹤楼店)2. Seesaw Coffee"

tools = [
    Tool(name="地图查询", func=search_coffee, description="查询景点周边营业中的咖啡馆"),
    Tool(name="天气查询", func=lambda _: "明天晴,25°C", description="获取天气预报")
]

# 构建带记忆的 Agent
llm = Ollama(model="qwen:7b")  # 本地运行,免 API 费用
agent = initialize_agent(
    tools=tools,
    llm=llm,
    agent="conversational-react-description",
    memory=memory,
    verbose=True
)

# 用户交互
print(agent.run("我不喜欢爬山。"))
print(agent.run("我想明天去黄鹤楼,附近有开着的咖啡馆吗?"))

此代码可在本地用 Ollama + LangChain 快速验证,确保“不爬山”偏好被保留并影响后续推荐。

🧠 更精细的意图解析:结构化字段提取(no_example 补充)
对于复杂旅游请求(如“上海三日游,预算2000,带老人”),可借助 PydanticOutputParser 提取结构化字段:

from pydantic import BaseModel, Field
from langchain.output_parsers import PydanticOutputParser

class TravelIntent(BaseModel):
    destination: str = Field(description="目的地城市或景点")
    days: int = Field(description="行程天数")
    budget: int = Field(description="总预算(元)")
    constraints: list[str] = Field(default_factory=list, description="限制条件,如'带老人''不吃辣'")

parser = PydanticOutputParser(pydantic_object=TravelIntent)
prompt = PromptTemplate.from_template(
    "请从用户问题中提取行程信息。\n{format_instructions}\n用户问题:{query}"
).partial(format_instructions=parser.get_format_instructions())

chain = prompt | llm | parser
result = chain.invoke({"query": "帮我规划上海三日游,预算2000,要适合老人"})
# 输出:TravelIntent(destination='上海', days=3, budget=2000, constraints=['适合老人'])

提取后的结构化数据可直接用于调度不同子 Agent(如酒店搜索、无障碍路线规划),大幅提升系统鲁棒性。

MCP:安全调用外部工具的新标准

MCP 是由 Anthropic 提出的一种标准化协议,旨在为 LLM 安全、统一地调用外部工具提供规范——你可以把它理解为“AI 工具交互的 HTTP 协议”。

与直接在代码中硬编码 API 调用相比,MCP 的优势在于:

  • 权限隔离:工具服务运行在独立进程中,LLM 仅能通过结构化请求调用,无法直接访问密钥或内部逻辑;
  • 标准化接口:所有工具遵循统一的输入/输出格式,便于插拔和测试;
  • 可观测性:每一步工具调用均可被记录、审计和重放。

🔍 MCP vs. 传统 LangChain Tool(vague_concept 补充)
对初学者而言,选择 MCP 还是自定义 Tool 取决于项目阶段:

维度 LangChain 自定义 Tool MCP
通信机制 直接函数调用(Python 内存) 标准化 JSON-RPC over stdio
消息格式 自定义参数 固定 schema:{"tool_name": "...", "parameters": {...}, "context_id": "..."}
安全性 低(API 密钥易暴露) 高(密钥封装在服务端,支持鉴权)
调试体验 依赖日志 支持 LangSmith 全链路追踪
适用场景 原型验证、Demo 生产环境、多工具集成

例如,若你只是本地测试,直接写 Tool(func=call_amap_api) 更高效;但若要部署到线上服务,强烈建议使用 MCP,通过 npx @modelcontextprotocol/server-gaode 启动高德服务,LangChain 通过 stdio 安全通信,避免密钥泄露风险。

🛠️ MCP 服务端部署详解(vague_concept 补充)
要实际运行 MCP 服务,需完成以下步骤:

  1. 安装 Node.js 环境(>=18.x)
  2. 配置高德开发者密钥(AMAP_KEY)
  3. 执行命令启动 MCP 服务:
    npx @modelcontextprotocol/server-gaode --amap-key YOUR_AMAP_KEY
    
  4. LangChain 客户端通过 langchain-mcp-adapters 自动建立 stdio 通道,无需手动构造 HTTP 请求。

MCP 请求示例(由 LangChain 自动生成):

{
  "tool_name": "search_pois",
  "parameters": {
    "keywords": "咖啡",
    "location": "114.3054,30.5931",
    "radius": 1000
  },
  "context_id": "conv_12345"
}

响应同样为结构化 JSON,确保 LLM 仅处理可信数据,杜绝幻觉源头。

在 LangChain 生态中,官方已推出 langchain-mcp-adapters 开源项目,可轻松桥接 MCP 服务(如通过 npx @modelcontextprotocol/server-gaode 启动的高德地图服务)与 LangChain Agent。例如:

# 启动高德 MCP 服务(需配置 AMAP_KEY)
npx @modelcontextprotocol/server-gaode

LangChain 客户端即可通过 stdio 与之通信,无需手动处理 API 请求细节,大幅降低集成复杂度并提升安全性。

开源项目

对初学者而言,建议优先参考官方资源

这些项目维护活跃、文档齐全,更适合学习和复现。

从工具调用到行程生成:完整链路示例(no_example 补充)

很多教程止步于“调用 API 获取数据”,但如何让 LLM 把多源信息整合成连贯行程才是关键。以下是一个完整提示词模板,显式融合景点、预算、交通与用户偏好:

from langchain.prompts import ChatPromptTemplate

final_prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一名专业旅游规划师。请根据以下信息生成个性化一日游行程:\n"
               "- 用户偏好:{user_preferences}\n"
               "- 景点列表:{poi_list}\n"
               "- 交通时间:{transit_time}分钟\n"
               "- 预算限制:{budget}元\n"
               "要求:\n"
               "1. 按时间顺序排列\n"
               "2. 标注每个地点的停留时间和花费\n"
               "3. 总花费不超过预算\n"
               "4. 避免任何需爬山的活动"),
    ("human", "请生成明天的行程")
])

# 假设已通过工具获取以下数据
context = {
    "user_preferences": "不喜欢爬山,喜欢咖啡馆",
    "poi_list": "黄鹤楼(门票70元,游览1.5小时)、东湖绿道(免费,骑行)、Seesaw Coffee(人均40元)",
    "transit_time": "20",
    "budget": "200"
}

formatted_prompt = final_prompt.format(**context)
response = llm.invoke(formatted_prompt)
print(response.content)

输出示例
“为您规划明日武汉一日游:
9:00-10:30 黄鹤楼(门票70元,平缓步道,无爬山)
11:00-12:30 东湖绿道骑行(免费,湖景优美)
13:00 Seesaw Coffee 午餐+休息(人均40元)
总花费约110元,符合200元预算。”

此方法通过结构化上下文注入,确保 LLM 不再“自由发挥”,而是基于真实数据生成可靠建议。

使用 RAG 构建本地旅游知识库(no_example 补充)

当通用 LLM 缺乏特定区域深度知识(如小众景点开放时间、本地特色美食)时,可结合 RAG(检索增强生成)技术构建本地知识库:

  1. 准备数据:整理景点 CSV 文件,包含字段:name, description, opening_hours, tags(如“亲子”“无障碍”“需预约”)。
  2. 嵌入与存储
    from langchain_community.vectorstores import Chroma
    from langchain_community.embeddings import HuggingFaceEmbeddings
    import csv
    
    # 加载景点数据
    docs = []
    with open("wuhan_attractions.csv", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        for row in reader:
            content = f"{row['name']}: {row['description']}。开放时间:{row['opening_hours']}。标签:{row['tags']}"
            docs.append(content)
    
    # 分块并嵌入(此处简化为整条记录)
    embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
    vectorstore = Chroma.from_texts(docs, embeddings, persist_directory="./chroma_db")
    retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
    
  3. 在 Agent 中集成检索
    from langchain.tools import Tool
    
    def retrieve_local_attractions(query: str) -> str:
        results = retriever.invoke(query)
        return "\n".join(results)
    
    local_knowledge_tool = Tool(
        name="LocalAttractionDB",
        func=retrieve_local_attractions,
        description="查询武汉本地景点详细信息,包括开放时间、特色和适用人群"
    )
    
  4. 组合使用:当用户问“武汉有哪些适合带婴儿的景点?”,Agent 先调用 LocalAttractionDB 检索含“亲子”“无障碍”标签的景点,再结合高德地图获取实时位置与营业状态,最终生成精准推荐。

📌 RAG 优势

  • 解决 LLM 知识截止问题(如2024年新开的景点)
  • 支持高度个性化(如“清真餐厅”“宠物友好”)
  • 降低幻觉率:LLM 仅基于检索到的真实数据生成回答

🧪 RAG 与用户偏好融合技巧(no_example 补充)
为提升检索相关性,可将用户约束拼接到查询语句中:

# 从记忆中提取偏好
user_prefs = memory.load_memory_variables({})["chat_history"]
enhanced_query = f"适合带婴儿的景点,要求无障碍设施完善。{user_prefs}"
relevant_docs = retriever.invoke(enhanced_query)

此方法使向量检索不仅匹配关键词,还理解上下文约束,显著提升推荐精准度。

# 示例:定义高德地图工具(简化版)
from langchain.tools import Tool
import requests

def search_pois_nearby(query: str, location: str) -> str:
    # 实际项目中应使用高德 Web API + 密钥
    url = f"https://restapi.amap.com/v5/place/text?key=YOUR_KEY&keywords={query}&city=武汉"
    resp = requests.get(url).json()
    return "\n".join([f"{item['name']} ({item['address']})" for item in resp["pois"][:3]])

coffee_tool = Tool(
    name="SearchCoffeeShops",
    func=lambda _: search_pois_nearby("咖啡", "黄鹤楼"),
    description="查询黄鹤楼附近正在营业的咖啡馆"
)

# 记忆配置
from langchain.memory import ConversationBufferMemory
memory = ConversationBufferMemory(memory_key="chat_history")

# 构建 Agent(使用 ReAct 策略)
from langchain.agents import initialize_agent
agent = initialize_agent(
    tools=[coffee_tool],
    llm=your_llm,
    agent="react-docstore",
    memory=memory,
    verbose=True
)

🔧 技术选型小贴士

  • 对于本地开发,可用 Ollama 运行 Qwen、Llama3 等开源模型,避免 API 费用;
  • 高德地图集成推荐两种方式:一是通过 MCP(Model Context Protocol)npx 启动本地服务封装 API,二是直接编写自定义 Tool 类;
  • 调试时强烈建议接入 LangSmith,可视化每一步工具调用与推理链,快速定位“为何推荐了已关闭店铺”。

学会这个判断思维,你就避开了90%的“AI翻车”陷阱。下一章,我们将动手用 LangChain 搭建一个真正“会查地图+记对话”的旅游智能体,让它不再胡说八道!


聪明解法:用 LangChain 搭建“会查地图+记对话”的智能体

你有没有遇到过这样的情况:问旅游助手“上海有什么好玩的?”,它热情推荐了东方明珠、外滩,还顺带安利了佘山——可你明明刚说过“不想爬山”!上一章我们看到,如果让大模型“自由发挥”,它就像个健忘又固执的朋友,不仅记不住你的偏好,还可能胡编乱造景点信息。那有没有办法让它既聪明又靠谱呢?

答案是:有!关键在于给它配上“手脚”和“记忆本”。这正是 LangChain 的用武之地。

像搭乐高一样组装 AI 能力

LangChain 就像一套 AI 乐高积木。你可以把不同的功能模块(比如查地图、记对话、算路线)一块块拼起来,组成一个真正有用的智能助手。它不是魔法,而是一种模块化设计思想——把复杂任务拆解成小零件,再灵活组合。

💡 小贴士:并非所有场景都需要完整 Agent。如果只是回答“黄鹤楼在哪个城市?”这类静态问题,直接调用 LLM 更高效;只有涉及多工具协作、动态规划(如“帮我安排三天不爬山的上海行程”)时,才值得引入 Agent 架构。

三大核心:大脑、手脚、指挥官

想象你要组织一次班级春游:

  • LLM(大语言模型)是“大脑”:负责理解问题、组织语言,比如听懂你说“找附近不爬山的景点”。
  • Tool(工具)是“手脚”:比如高德地图 API,能实时查景点位置、算步行路线,就像派同学去实地踩点。
  • Agent(智能体)是“指挥官”:它决定什么时候该调用哪个工具。比如先查“上海室内景点”,再排除需要爬山的,最后生成推荐。

📚 学习重点: Agent 的核心不是“自己知道一切”,而是“知道该问谁、该用什么工具”。

图:LangChain Agent 核心架构。用户输入 → Agent 调度器 → 调用 Tool(如高德地图)→ 返回结构化结果 → LLM 生成自然语言回答 → 输出给用户。各模块职责清晰分离,便于扩展与调试。

下面,我们将严格按照 “大脑 → 手脚 → 指挥官” 的逻辑顺序,逐步构建这个智能体。


第一步:LLM 是“大脑”——理解意图但不能行动

大语言模型(如 GPT-4o)擅长理解自然语言并生成流畅回复,但它无法直接访问外部世界。例如:

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o")
response = llm.invoke("上海有哪些免费博物馆?")
print(response.content)

这段代码会返回一个看似合理的答案,但内容可能过时、错误,甚至虚构(如“浦东艺术馆”实际不存在)。LLM 只能基于训练数据“猜测”答案,无法验证事实

因此,我们需要为它装上“手脚”——也就是 Tool


第二步:Tool 是“手脚”——让 AI 能查地图、看天气

Tool 是连接 LLM 与外部世界的桥梁。在 LangChain 中,Tool 必须满足两个条件:

  1. 有明确的名称(name
  2. 有清晰的描述(description),告诉 LLM 何时使用它
方式一:自定义 Tool 类(适合快速原型)

以高德地图景点搜索为例,我们封装一个 AmapPlaceSearchTool

from langchain.tools import BaseTool
import requests

class AmapPlaceSearchTool(BaseTool):
    name = "amap_place_search"
    description = "根据关键词和城市搜索景点,返回名称、地址和是否需爬山"

    def _run(self, query: str) -> str:
        url = "https://restapi.amap.com/v5/place/text"
        params = {
            "key": "你的高德API_KEY",
            "keywords": query,
            "city": "上海",
            "types": "风景名胜|博物馆|展览馆"
        }
        resp = requests.get(url, params=params).json()
        results = []
        for poi in resp.get("pois", [])[:3]:
            name = poi["name"]
            address = poi["address"]
            # 简单规则:名称含“山”“峰”视为需爬山
            needs_hiking = "山" in name or "峰" in name
            results.append(f"{name}{address}{'【需爬山】' if needs_hiking else ''}")
        return "\n".join(results)

优势:代码简单,5 分钟即可跑通。
⚠️ 局限:API Key 硬编码在代码中,存在泄露风险;无法跨语言复用。

方式二:MCP 协议集成(生产级推荐)

MCP(Model Context Protocol)是一种新兴的社区驱动工具调用协议,受到业界对安全、结构化工具交互需求的启发(如 Anthropic 提出的工具使用原则)。它通过独立进程运行工具服务,彻底隔离敏感信息,目前尚未成为官方标准,但在 LangChain 社区和部分初创项目中已被积极采用。

与传统 Tool 的关键区别

维度 自定义 Tool MCP 协议
通信机制 Python 函数内联调用 stdio / WebSocket 进程间通信
安全性 API Key 需写入代码 密钥完全隔离在 LLM 上下文之外
部署灵活性 仅限 Python 支持 Node.js、Go 等任意语言封装工具
适用阶段 快速原型 生产环境、团队协作

MCP 工作流程

  1. 启动 MCP 服务端(如 npx @modelcontextprotocol/cli --tool=amap
  2. LangChain 通过 langchain-mcp-adapters 连接该服务
  3. LLM 生成的工具调用请求被自动转换为标准 JSON:
    {
      "tool_name": "amap_place_search",
      "parameters": {"query": "上海博物馆", "city": "上海"},
      "context_id": "sess_12345"
    }
    
  4. MCP 服务执行真实 API 调用并返回结构化结果

🌟 最佳实践:初学者用自定义 Tool 快速验证想法;项目进入测试阶段后,立即迁移到 MCP 以提升安全性和可维护性。截至 2024 年中,langchain-mcp-adapters 已支持包括高德地图、WeatherAPI、Google Calendar 等 10+ 个常用工具的 MCP 封装模板,是当前最活跃的参考实现。

无论哪种方式,注册工具后,LLM 就具备了“手脚”——但它还不知道何时该用哪只手。这就需要“指挥官”登场。


第三步:Agent 是“指挥官”——动态调度工具

Agent 的核心能力是 ReAct(Reason + Act)循环

  1. 思考(Thought):分析当前目标和已有信息
  2. 行动(Action):决定调用哪个 Tool
  3. 观察(Observation):接收 Tool 返回的结果
  4. 重复上述过程,直到能生成最终答案
基础 Agent 示例
from langchain.agents import initialize_agent, AgentType
from langchain_openai import ChatOpenAI

tools = [AmapPlaceSearchTool()]
llm = ChatOpenAI(model="gpt-4o")

# 使用 OpenAI Functions Agent(支持结构化工具调用)
agent = initialize_agent(
    tools, 
    llm, 
    agent=AgentType.OPENAI_FUNCTIONS,
    verbose=True  # 打印 ReAct 过程
)

response = agent.run("上海有哪些不爬山的免费博物馆?")

输出日志示例

Thought: 用户需要上海的免费博物馆,且不能爬山。应调用 amap_place_search 工具。
Action: amap_place_search
Action Input: {"query": "博物馆"}
Observation: 上海博物馆(人民大道201号)\n中华艺术宫(上南路205号)【需爬山】\n上海当代艺术博物馆(苗江路678号)
Thought: 已获取结果,过滤掉需爬山的选项。
Final Answer: 推荐以下两个免费博物馆:1. 上海博物馆(人民大道201号);2. 上海当代艺术博物馆(苗江路678号)。

🔍 关键洞察:Agent 并非“一次性回答”,而是通过多轮推理逼近正确答案。这正是它优于普通 LLM 调用的核心价值。


第四步:加入“记忆本”——记住用户偏好

光会查地图还不够!真正的贴心在于记住对话历史。LangChain 提供了 ConversationBufferMemory 这样的“记忆本”,能把你说过的“不喜欢爬山”“想看博物馆”都存下来。

对话记忆如何工作?

ConversationBufferMemory 会将完整的对话历史以 chat_history 变量形式注入到每次提示词(Prompt)中。例如:

from langchain.memory import ConversationBufferMemory
from langchain.agents import AgentExecutor

memory = ConversationBufferMemory(
    memory_key="chat_history", 
    return_messages=True
)

# 创建带记忆的 Agent
agent_with_memory = AgentExecutor.from_agent_and_tools(
    agent=agent.agent,
    tools=tools,
    memory=memory,
    verbose=True
)

# 用户第一次说:“我不想去爬山的地方”
agent_with_memory.run("我不想去爬山的地方")
# 后续提问:“上海有什么好玩的?”
response = agent_with_memory.run("上海有什么好玩的?")

此时,LLM 的上下文包含:

Human: 我不想去爬山的地方
AI: 好的,我会避开山岳类景点。
Human: 上海有什么好玩的?

因此,即使后续提问未提及“爬山”,Agent 仍会主动过滤佘山、辰山等地点。

⚠️ 注意:ConversationBufferMemory 会随对话变长而增加 token 消耗。若对话轮次较多,建议改用 ConversationSummaryMemory 或带滑动窗口的 ConversationTokenBufferMemory

高级技巧:用 RAG 增强个性化记忆

对于更复杂的偏好(如“喜欢文艺小众”),可结合 RAG 技术:将用户历史偏好向量化存入本地向量数据库(如 Chroma),每次查询时检索最相关的偏好片段,再注入 Prompt。

实现步骤

  1. 用户说“我喜欢思南公馆那种文艺街区”
  2. 系统将该语句嵌入为向量,存入 Chroma
  3. 下次提问“推荐类似的地方”时,自动检索“文艺”“小众”“咖啡馆”等关键词匹配的偏好
  4. 将检索结果作为上下文注入 Agent
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor

# 初始化向量库(首次运行时为空)
vectorstore = Chroma(embedding_function=OpenAIEmbeddings(), persist_directory="./preference_db")

# 当用户表达偏好时,存入向量库
user_preference = "我喜欢思南公馆那种文艺街区,有老建筑和独立咖啡馆"
vectorstore.add_texts([user_preference])

# 查询时检索相关偏好
retriever = vectorstore.as_retriever(search_kwargs={"k": 1})
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
    base_compressor=compressor, base_retriever=retriever
)

relevant_prefs = compression_retriever.get_relevant_documents("推荐类似的地方")
# 将 relevant_prefs 注入 Agent 的 prompt

这种方式比纯对话记忆更精准,尤其适合长期用户画像构建。


第五步:实战整合——从“查景点”到“生成行程”

现在,我们将所有模块组合成一个端到端系统:

  1. 用户输入:“从豫园怎么去上海博物馆?我讨厌坐地铁。”
  2. Agent 解析意图:识别出“路线规划”需求,并注意到“讨厌坐地铁”这一约束。
  3. 调用高德地图工具:使用封装好的 AmapRouteTool,指定交通方式为“步行”或“公交”(排除地铁)。
  4. 工具返回结构化数据
    {
      "distance": "1.2公里",
      "duration": "15分钟",
      "steps": ["沿方浜中路向东", "右转进入河南南路", "..."]
    }
    
  5. LLM 生成人性化回答:结合记忆与工具结果,输出:

    “推荐步行15分钟,全程1.2公里,不经过地铁站!沿途还能逛城隍庙小吃街,试试南翔小笼包哦~”

🌟 真实案例参考:虽然 GitHub 上存在项目如 wzycoding/langchain-travel-agent,但截至 2024 年中,该项目 Star 数不足 50,且最近一次更新已超过 6 个月,功能完整性有限。建议初学者优先参考 LangChain 官方 langchain-mcp-adapters 项目,其文档完善、持续维护,并提供高德地图 MCP 的标准集成模板,可确保可靠性和可复现性。


通过这一章,你已经掌握了构建智能体的核心逻辑:用模块化思维,把 LLM(大脑)、Tool(手脚)、Agent(指挥官)和 Memory(记忆本)有机组合。接下来,我们将进一步升级——加入本地知识库,让助手不仅能联网查信息,还能读懂你私藏的旅行攻略!


实战升级:加入本地知识库(RAG)和可视化调试(LangSmith)

你有没有遇到过这种情况?朋友问你:“上海有哪些小众又好看的咖啡馆?”你打开手机搜了一圈,结果全是网红打卡点,真正安静又有特色的店却找不到。其实,大模型也一样——它知道很多“公开信息”,但对那些藏在旅游攻略PDF、本地博主网页里的“宝藏内容”,它就“看不见”了。

这时候,我们就需要给AI助手装上一个“私人图书馆”!

📚 让AI学会“查资料”:RAG技术

想象一下,你有一本自己整理的《上海小众旅行手账》,里面有你收藏的咖啡馆、老弄堂、独立书店……现在,我们可以把这本“手账”(比如PDF或网页)变成AI能读懂的格式。这就是RAG(检索增强生成)——当用户提问时,AI先去你的“私人图书馆”里找相关资料,再结合这些信息回答问题。

比如,用户问“推荐静安寺附近的小众咖啡馆”,AI会先从你上传的旅游攻略中找出匹配的内容,再调用高德地图确认位置和营业时间,最后给出精准推荐。这样,答案就不再是泛泛而谈,而是有依据、有个性、更可信

高德地图工具 AI智能体 RAG检索模块 用户 高德地图工具 AI智能体 RAG检索模块 用户 提问(如“静安寺附近小众咖啡馆”) 检索本地知识库(PDF/网页) 返回相关资料片段 调用高德地图查位置和营业时间 返回地理信息 规划回答步骤 生成最终个性化回答

RAG与AI智能体协作流程:用户提问后,系统先检索本地知识库,再结合高德地图工具,由Agent规划并生成精准回答

要实现这一点,你可以使用 LangChain 的 RetrievalQA 链配合向量数据库(如 FAISS 或 Chroma)。例如,将 PDF 内容通过 PyPDFLoader 加载,用 RecursiveCharacterTextSplitter 分块,再用 OpenAIEmbeddings 或本地模型(如 BGE)生成嵌入向量存入数据库。当用户提问时,系统自动检索最相关的片段作为上下文注入提示词:

from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain.chains import RetrievalQA
from langchain_openai import ChatOpenAI

# 加载并切分PDF
loader = PyPDFLoader("shanghai_cafes_guide.pdf")
docs = loader.load()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
splits = text_splitter.split_documents(docs)

# 构建向量库
vectorstore = FAISS.from_documents(splits, OpenAIEmbeddings())
retriever = vectorstore.as_retriever()

# 创建RAG链
qa_chain = RetrievalQA.from_chain_type(
    llm=ChatOpenAI(model="gpt-4o"),
    retriever=retriever,
    chain_type="stuff"
)
response = qa_chain.invoke("静安寺附近有哪些小众咖啡馆?")

这样,你的AI助手就能基于你私有的知识库回答问题,而不是依赖互联网上的泛化信息。

🌐 从 PDF 与网页构建知识库:不止于 CSV

虽然结构化数据(如 CSV)便于管理,但真实世界中的“宝藏信息”往往藏在非结构化文档中——比如一篇深度游记博客、一份 PDF 版城市指南,或一个本地生活公众号文章。为此,我们需要支持多源异构数据的加载与处理。

1. 从 PDF 提取内容(已展示)
使用 PyPDFLoader 可高效解析文本型 PDF。对于扫描版 PDF,可结合 OCR 工具(如 pdf2image + pytesseract)预处理。

2. 从网页抓取内容
使用 WebBaseLoader 自动提取网页正文,过滤广告与导航栏:

from langchain_community.document_loaders import WebBaseLoader

# 加载多个本地博主推荐的咖啡馆页面
urls = [
    "https://example.com/shanghai-hidden-cafes",
    "https://travel.blog.com/wukang-road-guide"
]
loader = WebBaseLoader(urls)
docs = loader.load()

# 后续切分与嵌入流程与 PDF 相同
text_splitter = RecursiveCharacterTextSplitter(chunk_size=600, chunk_overlap=60)
splits = text_splitter.split_documents(docs)

3. 使用 EmbedChain 快速构建知识库(补充)
如果你希望更快速地搭建 RAG 系统,可考虑 EmbedChain —— 一个专为 RAG 设计的轻量级框架。它封装了文档加载、切分、嵌入、检索全流程,一行代码即可添加多种数据源:

from embedchain import App

app = App.from_config(config_path="config.yaml")  # 配置嵌入模型与向量库
app.add("path/to/shanghai_cafes_guide.pdf")       # 添加 PDF
app.add("https://example.com/shanghai-hidden-cafes")  # 添加网页

# 直接问答
response = app.query("静安寺附近有哪些小众咖啡馆?")
print(response)

EmbedChain 支持 Chroma、Pinecone 等后端,并内置对 BGE、OpenAI 等嵌入模型的支持,适合快速验证想法。但对于需要精细控制检索逻辑(如元数据过滤、混合检索)的生产场景,仍建议使用 LangChain 原生组件。

为什么非结构化数据如此重要?
公开 API(如高德地图)通常只提供基础字段(名称、地址、评分),而“氛围安静”“店主是爵士乐迷”“窗边有梧桐树影”等关键体验描述,往往只存在于博客、PDF 攻略或小红书笔记中。RAG 正是打通这些“沉默知识”的桥梁,让 AI 推荐更具人情味与可信度

🗺️ 旅游场景下的 RAG 实践:构建本地景点知识库

为了让 RAG 在旅游助手中发挥最大价值,你需要将结构化景点数据(如名称、简介、开放时间、标签、是否适合雨天等)转化为可检索的文本块。以下是一个完整示例,展示如何从 CSV 构建本地景点向量库:

import pandas as pd
from langchain_core.documents import Document
from langchain_community.vectorstores import Chroma
from langchain_huggingface import HuggingFaceEmbeddings  # 使用开源嵌入模型

# 1. 准备本地景点数据(例如 shanghai_pois.csv)
# name,address,description,tags,open_time,fee
# 上海博物馆,人民广场,中国顶级历史艺术博物馆,"博物馆,室内,免费",9:00-17:00,免费
# 武康路老建筑群,徐汇区,梧桐树下的百年洋房,"文艺,散步,拍照",全天,免费

df = pd.read_csv("shanghai_pois.csv")
documents = []
for _, row in df.iterrows():
    content = f"景点名称:{row['name']}\n地址:{row['address']}\n描述:{row['description']}\n标签:{row['tags']}\n开放时间:{row['open_time']}\n费用:{row['fee']}"
    documents.append(Document(page_content=content, metadata={"name": row["name"]}))

# 2. 切分并嵌入(使用开源 BGE 模型,无需 API Key)
text_splitter = RecursiveCharacterTextSplitter(chunk_size=300, chunk_overlap=30)
splits = text_splitter.split_documents(documents)

embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5")
vectorstore = Chroma.from_documents(splits, embeddings)

# 3. 检索示例:用户问“雨天适合去哪?”
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
relevant_docs = retriever.invoke("上海雨天适合逛的室内景点")
print("\n".join([doc.page_content for doc in relevant_docs]))

通过这种方式,即使高德地图未返回“适合雨天”的标签,你的本地知识库也能精准匹配用户需求,显著提升推荐的相关性与个性化程度。

如何将用户偏好融入 RAG 检索?
假设用户说:“我喜欢亲子游,预算5000元。”你可以在检索前动态构造查询语句:

user_query = "上海适合亲子游的景点,预算友好"
relevant_docs = retriever.invoke(user_query)

更高级的做法是将用户偏好作为元数据过滤条件。例如,在文档创建时添加 {"category": "family-friendly"},然后使用 Chroma 的 filter 参数:

retriever = vectorstore.as_retriever(
    search_kwargs={"k": 3, "filter": {"category": "family-friendly"}}
)

🧠 复杂任务怎么办?用 LangGraph 管理“思考步骤”

如果用户说:“帮我规划一个三天两夜的上海行程,第一天看外滩,第二天逛博物馆,第三天找家安静的咖啡馆发呆。”这可不是一句话能搞定的!AI需要分步骤处理:先查景点 → 再找附近酒店 → 最后安排交通和休息点。

这时候,LangGraph 就像一个“任务导演”,帮AI理清每一步该做什么、什么时候切换状态。它让智能体不再“想到哪做到哪”,而是有条不紊地完成多阶段任务。

更重要的是,你需要判断是否真的需要 Agent。如果是简单问题(如“黄鹤楼在哪”),直接调用 LLM 更高效;只有涉及多工具协作、动态规划(如行程定制、预算分配)时,才启用完整 Agent 架构。

一个典型的端到端流程如下:

  1. 用户输入:“我想去上海玩三天,预算3000元,不喜欢爬山。”
  2. Router Agent 解析意图:识别出目的地(上海)、天数(3天)、预算(3000元)、禁忌(不爬山)。
  3. 调度工具
    • 调用高德地图 MCP 服务查询“上海博物馆”“外滩”等景点;
    • 使用自定义工具过滤含“登山”“徒步”的景点;
    • 调用高德酒店 API 获取附近住宿价格。
  4. 整合信息:LLM 将结构化数据转化为自然语言行程建议。
📡 MCP vs. 传统 Tool:通信机制与消息格式对比
特性 传统 LangChain Tool MCP(Model Context Protocol)
通信方式 Python 函数直接调用 标准输入/输出(stdin/stdout)流
消息格式 无固定格式,依赖函数签名 统一 JSON Schema:
{"tool_name": "...", "parameters": {...}, "context_id": "..."}
安全性 密钥暴露在代码中,需自行管理 密钥仅存在于 MCP 服务端,LLM 无权访问
部署模式 与主程序同进程 独立服务进程,支持跨语言(如 Node.js 实现高德 MCP)
适用场景 快速原型、内部测试 生产环境、涉及敏感 API 或用户隐私

MCP 服务端典型工作流程

  1. 启动 MCP 服务(如 npx @modelcontextprotocol/cli@latest --server gaode-map
  2. 服务注册可用工具列表(如 gaode_map.search_poi, gaode_map.route_planning
  3. 接收 LLM 发来的 JSON 请求:
    {
      "tool_name": "gaode_map.search_poi",
      "parameters": {"city": "上海", "keyword": "博物馆"},
      "context_id": "req_12345"
    }
    
  4. 执行高德 API 调用,验证参数合法性,记录审计日志
  5. 返回结构化响应:
    {
      "result": [{"name": "上海博物馆", "address": "人民广场"}],
      "error": null,
      "context_id": "req_12345"
    }
    

在 LangChain 生态中,官方已推出 langchain-mcp-adapters 开源项目,可自动将 MCP 服务器暴露的工具转换为 LangChain 兼容的 Tool 对象,大幅简化集成流程。

MCP vs. 传统 LangChain Tool:何时选择?

  • LangChain 自定义 Tool:适合快速原型开发,代码内联、调试直观,但密钥管理、错误处理需自行实现,安全性较低。
  • MCP 服务:适合生产环境,支持热更新、跨语言调用、细粒度权限控制,且天然隔离敏感操作,但需额外部署服务进程。
    对于旅游助手这类涉及真实地理位置、用户隐私和第三方 API 调用的场景,强烈推荐使用 MCP,以保障系统安全性和可维护性。

要调用高德地图 API,你可以通过两种方式集成到 LangChain:

  • 方式一:MCP(Model Context Protocol)
    在本地运行 npx @modelcontextprotocol/cli@latest 启动高德 MCP 服务端,LangChain 通过 stdio 与其通信,自动发现并调用 gaode_map.search_poi 等工具。这种方式更安全、更符合生产级规范。

  • 方式二:自定义 Tool
    直接封装高德 API 为 LangChain 工具(适合快速原型):

from langchain.tools import tool
import requests

@tool
def search_poi_in_city(city: str, keyword: str) -> str:
    """根据城市和关键词搜索兴趣点(POI)"""
    url = "https://restapi.amap.com/v5/place/text"
    params = {
        "key": "YOUR_GAODE_API_KEY",
        "keywords": keyword,
        "city": city,
        "limit": 5
    }
    resp = requests.get(url, params=params).json()
    pois = [f"{p['name']}{p['address']})" for p in resp.get("pois", [])]
    return "\n".join(pois) if pois else "未找到相关地点"

# 注册到Agent
tools = [search_poi_in_city]
🧭 如何实现 Router Agent?附可复现代码

Router Agent 的核心是意图识别。你可以通过一个专门的提示词模板引导 LLM 判断用户需求类型,并输出结构化指令。以下是一个可直接使用的实现示例:

from langchain.prompts import PromptTemplate
from langchain_openai import ChatOpenAI

# 定义路由提示词
router_prompt = PromptTemplate.from_template("""
你是一个旅游助手的路由模块,请根据用户的问题判断应调用哪个工具。
可用工具包括:
- itinerary_planner: 用户要求规划多日行程、包含天数、预算、偏好等
- poi_search: 用户询问具体地点、景点、咖啡馆等
- weather_check: 用户询问天气
- fallback: 无法判断或属于通用问答

请仅输出工具名称,不要解释。

用户问题:{input}
""")

def route_query(user_input: str) -> str:
    llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
    chain = router_prompt | llm
    response = chain.invoke({"input": user_input})
    return response.content.strip()

# 示例使用
query = "上海有哪些适合雨天逛的博物馆?"
tool_name = route_query(query)
print(f"应调用工具: {tool_name}")  # 输出: poi_search

完整意图解析:提取结构化字段
对于行程规划类请求,仅路由还不够,还需提取关键参数。以下是使用 PydanticOutputParser 的完整示例:

from pydantic import BaseModel, Field
from langchain.output_parsers import PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate

class TravelIntent(BaseModel):
    destination: str = Field(description="目的地城市,如'上海'")
    days: int = Field(description="行程天数,若未提及则为1", default=1)
    budget: int = Field(description="总预算(元),若未提及则为0", default=0)
    constraints: str = Field(description="用户禁忌或偏好,如'不爬山'、'喜欢咖啡馆'", default="")
    query_type: str = Field(description="查询类型", enum=["itinerary", "poi", "weather", "other"])

parser = PydanticOutputParser(pydantic_object=TravelIntent)
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个旅游助手,请从用户问题中精确提取以下信息:\n{format_instructions}"),
    ("human", "{input}")
]).partial(format_instructions=parser.get_format_instructions())

def parse_travel_intent(user_input: str) -> TravelIntent:
    chain = prompt | ChatOpenAI(temperature=0) | parser
    return chain.invoke({"input": user_input})

# 测试
intent = parse_travel_intent("上海三天两夜,预算2500,不想爬山")
print(intent)
# 输出: TravelIntent(destination='上海', days=3, budget=2500, constraints='不想爬山', query_type='itinerary')

该 Router 可作为 Agent 执行前的第一步,动态决定后续调用哪些工具链,避免不必要的计算开销。

然后使用 ReAct Agent 或 Plan-and-Execute 策略执行:

from langchain.agents import create_react_agent, AgentExecutor
from langchain import hub

prompt = hub.pull("hwchase17/react")
agent = create_react_agent(ChatOpenAI(), tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
result = executor.invoke({"input": "上海有哪些适合雨天逛的博物馆?"})

完整端到端行程生成示例(no_example 补充)
以下代码展示了如何将高德 API 返回的结构化数据与用户约束(预算、禁忌)融合,生成自然语言行程:

from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

# 假设已通过工具获取以下数据
user_budget = 3000
user_constraints = "不喜欢爬山,偏好文艺场所"
poi_list = [
    {"name": "上海博物馆", "address": "人民广场", "fee": "免费"},
    {"name": "武康路老建筑群", "address": "徐汇区", "fee": "免费"},
    {"name": "朵云书院", "address": "中心大厦52层", "fee": "免费"}
]

# 构建整合提示词
itinerary_prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一位资深上海旅行规划师。请根据以下信息生成一份一日游建议:\n"
               "- 用户预算:{budget} 元\n"
               "- 用户偏好/禁忌:{constraints}\n"
               "- 可选景点列表(含地址和费用):\n{poi_info}\n"
               "要求:按时间顺序安排,包含交通建议,总花费不超过预算,语言亲切自然。"),
    ("human", "请生成行程。")
])

# 格式化景点信息
poi_info = "\n".join([f"- {p['name']}{p['address']}{p['fee']})" for p in poi_list])

# 调用LLM生成行程
llm = ChatOpenAI(model="gpt-4o")
chain = itinerary_prompt | llm
response = chain.invoke({
    "budget": user_budget,
    "constraints": user_constraints,
    "poi_info": poi_info
})
print(response.content)

此模式确保 LLM 在生成答案时显式参考所有外部数据源,避免“幻觉”,同时保持输出的自然流畅。

🔍 调试神器:LangSmith 让AI“透明思考”

最头疼的问题是:AI到底怎么想的?为什么它推荐了那家咖啡馆?以前我们只能猜。但现在,LangSmith 能把AI的每一步思考过程“拍下来”——它调用了哪个工具?检索了哪些文档?推理逻辑是什么?全部可视化!

就像老师批改作文时看到你的草稿和思路,开发者也能通过LangSmith快速发现问题、优化流程。这不仅提升了准确性,也让AI变得更可解释、可信任

📊 LangSmith 实战:追踪 RAG + Agent 全链路

要启用 LangSmith,只需设置两个环境变量:

export LANGCHAIN_TRACING_V2=true
export LANGCHAIN_API_KEY=your_langsmith_api_key

💡 获取 API Key:登录 LangSmith 官网 → 点击右上角头像 → Settings → API Keys → Create API Key。

然后,所有使用 LangChain 构建的链(Chain)、Agent、RAG 流程都会自动上报到 LangSmith 控制台。

1. 查看单次调用的完整轨迹
在 LangSmith UI 中,你可以看到类似下图的调用树(Trace):

[IMAGE: LangSmith Trace 截图示意:根节点为用户输入 → 子节点包括 RAG 检索(显示 top-k 文档内容)→ 工具调用(如高德 POI 查询,显示请求参数与返回结果)→ LLM 生成最终回答]

2. 关键功能演示:

  • 检索内容可视化:点击 RAG 步骤,可查看实际检索到的文档片段及其相似度分数,确认是否命中关键信息。
  • 工具调用审计:查看每次 search_poi_in_city 调用的参数(如 city="上海", keyword="博物馆")和返回结果,验证 API 是否正常。
  • LLM 输入/输出对比:对比 LLM 接收到的完整提示词(含检索结果+工具返回)与最终输出,判断是否合理利用了上下文。
  • 性能分析:统计各步骤耗时,识别瓶颈(如嵌入生成慢、API 延迟高)。

3. 代码示例:带 LangSmith 追踪的 RAG + Tool 调用

import os
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_PROJECT"] = "Shanghai Travel Assistant"  # 自定义项目名

from langchain.chains import RetrievalQA
from langchain.agents import create_react_agent, AgentExecutor
from langchain import hub

# 假设 retriever 和 tools 已定义(如前文所示)
qa_chain = RetrievalQA.from_chain_type(
    llm=ChatOpenAI(model="gpt-4o"),
    retriever=retriever,
    chain_type="stuff",
    return_source_documents=True  # 便于 LangSmith 显示来源
)

# 构建 Agent
prompt = hub.pull("hwchase17/react")
agent = create_react_agent(ChatOpenAI(temperature=0), tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

# 执行查询 —— 所有步骤将自动上报至 LangSmith
result = executor.invoke({"input": "静安寺附近有哪些小众咖啡馆?"})

在 LangSmith 控制台,你将看到:

  • 检索阶段返回的 3 个最相关文档(来自你的 PDF/网页知识库)
  • Agent 决策过程(Thought → Action → Observation 循环)
  • 高德地图工具调用的具体参数与返回 POI 列表
  • 最终合成的回答及其依据

如何查看 Trace?

  1. 执行上述代码后,打开 LangSmith 控制台
  2. 进入 “Traces” 页面,筛选 Project 为 “Shanghai Travel Assistant”
  3. 点击任意一条 trace,即可展开完整的执行路径,包括每一步的输入、输出、耗时和元数据
  4. 可对 trace 添加 feedback(👍/👎),用于后续评估模型表现

可解释性 = 可信度
当用户看到“推荐理由:根据您上传的《上海咖啡手账》第12页提到‘%E8%8C%B6%E5%AE4’环境安静,且高德地图显示步行5分钟可达”,他们会更愿意相信这个建议。LangSmith 不仅是开发者的调试工具,更是构建用户信任的关键基础设施——它让 AI 的决策过程从“黑箱”变为“玻璃箱”。

💬 让AI“记住你说过的话”:对话记忆机制

你可能希望助手记住你的偏好,比如“我不想爬山”或“喜欢靠窗的咖啡馆”。这可以通过 LangChain 的记忆组件实现。

最常用的是 ConversationBufferMemory,它将整个对话历史作为上下文传给 LLM:

from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationChain

memory = ConversationBufferMemory()
conversation = ConversationChain(
    llm=ChatOpenAI(),
    memory=memory,
    verbose=True
)

conversation.predict(input="我不喜欢爬山")
conversation.predict(input="推荐上海的景点")  # LLM 会参考“不喜欢爬山”的上下文

对于长对话,可改用 ConversationSummaryMemory 或带滑动窗口的 ConversationTokenBufferMemory,避免上下文过长。

更高级的做法是将用户偏好结构化存储(如“禁忌:爬山”“偏好:文艺咖啡馆”),并在后续提示词中显式注入:

template = """
用户偏好:{user_preferences}
当前问题:{input}
请基于以上信息回答。
"""

这样,即使对话很长,关键信息也不会丢失。

“真正的智能不是‘全知全能’,而是‘知道自己不知道,并懂得去查’。”

📚 学习重点:

  • RAG = 给AI配“私人知识库”,解决公开数据不足的问题
  • LangGraph = 管理复杂任务的状态流转,像导演安排剧情
  • LangSmith = 可视化调试工具,让AI的思考过程一目了然,显著提升可解释性与用户信任
  • 记忆机制 = 用 ConversationBufferMemory 等组件让AI记住用户偏好
  • 工具集成 = 通过自定义 Tool 或 MCP 调用高德地图等外部服务
  • 多源知识构建 = 支持 PDF、网页、CSV 等多种格式,结合 EmbedChain 快速启动

掌握这些方法,你就不再是简单地“调用AI”,而是真正构建一个可靠、聪明、可维护的智能助手——而这,正是未来AI应用开发的核心能力。

💡 项目选型建议:虽然 GitHub 上存在如 wzycoding/langchain-travel-agent 等示例项目,但截至 2024 年中,该项目缺乏活跃维护(最近更新超过 6 个月,Star 数不足 100),功能完整性有限。建议初学者优先参考 LangChain 官方的 langchain-mcp-adapters 项目,其文档完善、更新频繁,并提供完整的 MCP 工具集成范例,更适合学习与生产复用。同时,务必结合 LangSmith 进行全流程追踪,这是保障系统可靠性与可解释性的关键实践。


总结

  • 不是所有问题都需要 Agent——先判断任务是否涉及多工具协作或动态规划
  • LangChain 提供模块化工具链,让高中生也能安全、高效地构建智能体
  • RAG 解决“知识盲区”,LangSmith 解决“黑箱困惑”,是提升可信度的关键
  • 从“能跑”到“可靠”,关键是理解每个组件的作用边界和协作逻辑

延伸阅读

尝试用 Streamlit 快速搭建 Web 界面,或探索 Ollama 本地运行 Qwen 模型,实现完全离线的旅游助手!

参考资料

🌐 网络来源

  1. https://python.langchain.com/
  2. https://docs.langchain.com/docs/
  3. https://langsmith.com/
  4. https://github.com/ollama/ollama
  5. https://lbs.amap.com/api/webservice/guide/api/direction

本文由 Vibe-Blog 自动发布

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐