手把手教你打造专属旅游小助手:小白也能玩转AI智能体!
注 : 本文纯由长文技术博客助手Vibe-Blog生成, 如果对你有帮助,你也想创作同样风格的技术博客, 欢迎关注开源项目: Vibe-Blog.
Vibe-Blog是一个基于多 Agent 架构的 AI 长文博客生成助手,具备深度调研、智能配图、Mermaid 图表、代码集成、智能专业排版等专业写作能力,旨在将晦涩的技术知识转化为通俗易懂的科普文章,让每个人都能轻松理解复杂技术,在 AI 时代扬帆起航.
手把手教你打造专属旅游小助手:高中生也能玩转AI智能体!

LangChain · Agent · RAG · LangGraph · 高德地图API · 旅游智能助手
阅读时间: 30 min
不是所有问题都需要复杂 Agent——学会判断何时用 LLM、何时用工具链,才是高中生开发智能应用的关键思维。
目录
- 问题来了:为什么我的“旅游助手”总在胡说八道?
- 高中生常见误区
- 本节学习目标
- 如何让AI“看得见”现实世界?——工具与记忆的实战方案
- Naive 方案翻车现场:直接让大模型“自由发挥”有多危险
- 案例一:推荐已关闭的景点
- 案例二:忽略用户显式约束
- 案例三:虚构营业信息
- 聪明解法:用 LangChain 搭建“会查地图+记对话”的智能体
- 第一步:LLM 是“大脑”——理解意图但不能行动
- 第二步:Tool 是“手脚”——让 AI 能查地图、看天气
- 第三步:Agent 是“指挥官”——动态调度工具
- 实战升级:加入本地知识库(RAG)和可视化调试(LangSmith)
- 📚 让AI学会“查资料”:RAG技术
- 🧠 复杂任务怎么办?用 LangGraph 管理“思考步骤”
- 🔍 调试神器:LangSmith 让AI“透明思考”
你是不是也幻想过,只要说一句“帮我规划三天上海行程”,就能立刻得到一份包含景点、交通、美食和酒店的完美攻略?过去这听起来像科幻,但现在,借助大语言模型(LLM)和开源框架 LangChain,高中生也能动手做出自己的“旅游智能助手”!
但别急——很多初学者一上来就堆代码、调 API,结果做出来的助手要么答非所问,要么卡在“查不到黄鹤楼在哪”。本文将带你从真实失败案例出发,一步步拆解问题根源,用清晰的逻辑和可视化流程图,教你如何用模块化思维构建一个真正聪明、能干活的旅游 AI 助手。
问题来了:为什么我的“旅游助手”总在胡说八道?
你有没有试过让AI帮你规划一次旅行?比如问它:“推荐一个上海三日游的行程!”结果它兴致勃勃地给你列了一堆北京的景点——故宫、天坛、颐和园……可你明明说的是上海啊!是不是觉得这AI有点“神志不清”?
别急,这其实不是AI“笨”,而是它有一个天生的“盲区”:它没有实时联网的能力,也分不清哪些是事实、哪些是自己“脑补”的故事。就像一个背了很多书但从来没出过门的学霸,他知道黄鹤楼在武汉,但他不知道今天黄鹤楼是否限流、门票多少钱,甚至可能把“外滩”记成“外滩公园”(其实并没有这个官方名字)。更糟糕的是,如果你问得模糊一点,比如“附近有什么好玩的?”,它根本不知道你此刻站在哪里!
由于当前文档环境无法嵌入图像,我们用 Mermaid 流程图替代原图,清晰展示“信息孤岛”问题:
💡 图中红色节点突出显示:LLM 在无外部工具支持下,处于“信息孤岛”状态,只能依赖静态知识库作答,极易产生事实性错误。
很多高中生朋友第一次接触大语言模型(LLM)时,会误以为它像电影里的超级AI一样“无所不能”——能查天气、订酒店、实时导航。但现实是:大模型只是一个“知识库”,不是“行动者”。它不能主动打开地图查路线,也不能打电话问景区营业时间。它只能根据训练时学到的旧知识,尽力“猜”出一个听起来合理的答案。
大模型是“知识库”,不是“行动者”——它知道黄鹤楼在武汉,但不会主动去查最新门票价格。
那么,什么时候可以直接用大模型回答问题?什么时候必须给它“装上工具”?这就引出了两个关键概念:
- 静态问答:比如“黄鹤楼在哪个城市?”“上海有哪些著名景点?”——这类问题答案固定,不随时间变化,大模型可以直接回答。
- 动态规划:比如“帮我安排明天从人民广场到迪士尼的路线,避开早高峰”“推荐今晚人均200元以内还能订到的餐厅”——这类问题需要实时数据和外部工具(如地图API、订餐平台),光靠大模型肯定不行。
📚 学习重点: 判断任务类型是构建智能助手的第一步。静态问题用LLM直接答,动态问题必须接入工具链!
常见误区
在教学实践中,我们发现在初次使用大模型开发旅游助手时,常陷入以下典型误区:
-
“AI 应该知道我在哪”
学生常假设 LLM 能自动获取用户地理位置。例如输入“附近有什么好吃的?”,却未提供任何位置上下文。实际上,LLM 无法访问设备 GPS 或 IP 地址(除非通过工具显式传入)。 -
“模型能实时更新知识”
有学生惊讶地发现:“我昨天问它上海天文馆开放时间,今天再问,答案还是错的!”——因为 LLM 的知识截止于训练数据(如 2023 年底),无法感知 2024 年新开放的场馆或临时闭馆通知。 -
“只要提示词写得好,AI 就不会胡说”
即使精心设计提示词(如“请只回答真实存在的景点”),LLM 仍可能因训练数据噪声生成虚构内容。例如,某开源模型曾将“上海玻璃博物馆”误记为“上海水晶宫”,并编造其位于“浦东新区世纪大道888号”(实际地址完全不同)。 -
“调用工具=万能解药”
有学生接入高德 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”会系统性失败?根本原因有三个:
- 没有外部数据源:LLM 的知识截止于训练时间(如 GPT-4 截止于2023年10月),无法获取实时信息(比如景点是否开放、天气如何、交通管制等)。
- 无状态记忆:标准对话接口中,LLM 不保留历史上下文。即使你上一句说“预算500元”,下一句提问时它仍当作全新对话处理。
- 不能调用工具:LLM 无法主动执行操作,如调用高德地图查POI、访问天气API、或比价酒店。它只能“凭空想象”答案。
这就像让你闭着眼睛画一张学校周边的地图——你记得教学楼在哪,但可能不知道新开的奶茶店已经取代了旧文具店!
💡 如何让 AI “记住”你说过的话?
实际开发中,可通过 LangChain 的ConversationBufferMemory组件显式保存对话历史。例如,当用户说“我不喜欢爬山”,系统会将该偏好存入内存,并在后续提示词中注入上下文:“用户偏好:避免爬山类活动”。这样,即使多轮对话后,AI 仍能保持一致性。更复杂的场景还可结合向量数据库(如 Chroma)实现基于语义的长期记忆,但这对简单旅游助手通常过度设计。
是否需要 Agent?—— 决策树与判断标准
别急着上复杂系统!先问自己一个问题:
这个任务是否需要“多步骤操作”或“查询外部最新信息”?
为帮助开发者快速判断,我们提供以下决策树(使用 Mermaid 实现):
判断标准对照表
| 问题类型 | 示例 | 是否需要 Agent | 理由 |
|---|---|---|---|
| 静态知识问答 | “黄鹤楼在哪个城市?” | ❌ 否 | 答案固定,无需外部数据 |
| 实时状态查询 | “黄鹤楼今天开放吗?” | ✅ 是 | 依赖当日营业状态 |
| 多约束规划 | “明天去黄鹤楼,不想爬山,附近有开着的咖啡馆吗?” | ✅ 是 | 需查地图+营业状态+记忆偏好 |
| 简单推荐 | “武汉有哪些著名景点?” | ❌ 否 | 通用知识足够 |
| 动态组合任务 | “帮我订一家离黄鹤楼步行10分钟、评分4.5以上、今晚有房的酒店” | ✅ 是 | 需调用酒店API+位置计算 |
📚 学习重点:判断是否需要 Agent 的关键,不是问题“看起来多复杂”,而是它是否依赖动态数据或分步执行。静态知识问答,LLM 足矣;动态任务协作,才需 Agent 出马。
如果只是问“黄鹤楼在哪”,别搞复杂架构——直接问 LLM 最快最稳!
一个真实可复现的端到端示例
假设用户输入:
“我想明天去黄鹤楼,但不想爬山,附近有开着的咖啡馆吗?”
一个基于 LangChain 的旅游 Agent 会这样工作:
- 意图解析:Router Agent 识别出关键需求 —— 目的地(黄鹤楼)、时间(明天)、禁忌(不爬山)、附加需求(咖啡馆)。
- 工具调度:
- 调用高德地图 API 查询“黄鹤楼”坐标及周边“咖啡馆”;
- 通过高德营业状态接口过滤“当前营业中”的店铺;
- (可选)调用天气 API 判断是否适合出行。
- 记忆注入:从
ConversationBufferMemory中提取“用户不喜欢爬山”,在生成行程时自动排除需登山的观景点。 - 结果整合: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 服务,需完成以下步骤:
- 安装 Node.js 环境(>=18.x)
- 配置高德开发者密钥(AMAP_KEY)
- 执行命令启动 MCP 服务:
npx @modelcontextprotocol/server-gaode --amap-key YOUR_AMAP_KEY- 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 请求细节,大幅降低集成复杂度并提升安全性。
开源项目
对初学者而言,建议优先参考官方资源:
- LangChain 官方文档中的 Agent 教程
langchain-mcp-adapters示例仓库(含高德、天气等真实工具集成)- LangChain 官方提供的 Travel Planner Template
这些项目维护活跃、文档齐全,更适合学习和复现。
从工具调用到行程生成:完整链路示例(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(检索增强生成)技术构建本地知识库:
- 准备数据:整理景点 CSV 文件,包含字段:
name,description,opening_hours,tags(如“亲子”“无障碍”“需预约”)。 - 嵌入与存储:
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}) - 在 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="查询武汉本地景点详细信息,包括开放时间、特色和适用人群" ) - 组合使用:当用户问“武汉有哪些适合带婴儿的景点?”,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 必须满足两个条件:
- 有明确的名称(
name) - 有清晰的描述(
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 工作流程:
- 启动 MCP 服务端(如
npx @modelcontextprotocol/cli --tool=amap) - LangChain 通过
langchain-mcp-adapters连接该服务 - LLM 生成的工具调用请求被自动转换为标准 JSON:
{ "tool_name": "amap_place_search", "parameters": {"query": "上海博物馆", "city": "上海"}, "context_id": "sess_12345" } - MCP 服务执行真实 API 调用并返回结构化结果
🌟 最佳实践:初学者用自定义 Tool 快速验证想法;项目进入测试阶段后,立即迁移到 MCP 以提升安全性和可维护性。截至 2024 年中,langchain-mcp-adapters 已支持包括高德地图、WeatherAPI、Google Calendar 等 10+ 个常用工具的 MCP 封装模板,是当前最活跃的参考实现。
无论哪种方式,注册工具后,LLM 就具备了“手脚”——但它还不知道何时该用哪只手。这就需要“指挥官”登场。
第三步:Agent 是“指挥官”——动态调度工具
Agent 的核心能力是 ReAct(Reason + Act)循环:
- 思考(Thought):分析当前目标和已有信息
- 行动(Action):决定调用哪个 Tool
- 观察(Observation):接收 Tool 返回的结果
- 重复上述过程,直到能生成最终答案
基础 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。
实现步骤:
- 用户说“我喜欢思南公馆那种文艺街区”
- 系统将该语句嵌入为向量,存入 Chroma
- 下次提问“推荐类似的地方”时,自动检索“文艺”“小众”“咖啡馆”等关键词匹配的偏好
- 将检索结果作为上下文注入 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
这种方式比纯对话记忆更精准,尤其适合长期用户画像构建。
第五步:实战整合——从“查景点”到“生成行程”
现在,我们将所有模块组合成一个端到端系统:
- 用户输入:“从豫园怎么去上海博物馆?我讨厌坐地铁。”
- Agent 解析意图:识别出“路线规划”需求,并注意到“讨厌坐地铁”这一约束。
- 调用高德地图工具:使用封装好的
AmapRouteTool,指定交通方式为“步行”或“公交”(排除地铁)。 - 工具返回结构化数据:
{ "distance": "1.2公里", "duration": "15分钟", "steps": ["沿方浜中路向东", "右转进入河南南路", "..."] } - 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会先从你上传的旅游攻略中找出匹配的内容,再调用高德地图确认位置和营业时间,最后给出精准推荐。这样,答案就不再是泛泛而谈,而是有依据、有个性、更可信。
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 架构。
一个典型的端到端流程如下:
- 用户输入:“我想去上海玩三天,预算3000元,不喜欢爬山。”
- Router Agent 解析意图:识别出目的地(上海)、天数(3天)、预算(3000元)、禁忌(不爬山)。
- 调度工具:
- 调用高德地图 MCP 服务查询“上海博物馆”“外滩”等景点;
- 使用自定义工具过滤含“登山”“徒步”的景点;
- 调用高德酒店 API 获取附近住宿价格。
- 整合信息: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 服务端典型工作流程
- 启动 MCP 服务(如
npx @modelcontextprotocol/cli@latest --server gaode-map)- 服务注册可用工具列表(如
gaode_map.search_poi,gaode_map.route_planning)- 接收 LLM 发来的 JSON 请求:
{ "tool_name": "gaode_map.search_poi", "parameters": {"city": "上海", "keyword": "博物馆"}, "context_id": "req_12345" }- 执行高德 API 调用,验证参数合法性,记录审计日志
- 返回结构化响应:
{ "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?
- 执行上述代码后,打开 LangSmith 控制台
- 进入 “Traces” 页面,筛选 Project 为 “Shanghai Travel Assistant”
- 点击任意一条 trace,即可展开完整的执行路径,包括每一步的输入、输出、耗时和元数据
- 可对 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 模型,实现完全离线的旅游助手!
参考资料
🌐 网络来源
- https://python.langchain.com/
- https://docs.langchain.com/docs/
- https://langsmith.com/
- https://github.com/ollama/ollama
- https://lbs.amap.com/api/webservice/guide/api/direction
本文由 Vibe-Blog 自动发布
更多推荐


所有评论(0)