一、项目背景与功能实现
本文分享基于 LangChain 框架开发的智能旅游助手 Agent,核心功能是「查询城市天气 + 根据天气推荐景点」,并对比 ReAct 与 FunctionCall 两种 Agent 实现模式的差异,同时总结 LangChain 框架的使用优势。
核心功能拆解
- 天气查询工具:调用 wttr.in 开源天气接口,获取指定城市实时天气(天气描述+温度),仅支持实时查询,忽略日期参数;
- 景点推荐工具:基于 Tavily 搜索引擎,构造「城市+天气+适合景点」查询词,获取精准的景点推荐结果;
- Agent 逻辑控制:限制每个城市的工具调用次数(天气/景点各1次),获取有效景点结果后直接返回最终回答,避免重复调用;
- 前端交互:基于原生 HTML/JS 实现简洁的对话界面,支持消息发送、加载状态、对话重置。
核心文件技术细节
1. lc_react.py:ReAct 模式
ReAct 模式的核心是「思考-行动-观察」循环,通过提示词约束 LLM 输出固定格式的文本(Thought/Action/Action Input),再解析文本执行工具。
关键函数:
_json_objects_in_text:从 LLM 输出的杂乱文本中提取 JSON 对象(解决 LLM 易拼接无关文本的问题);
_weather_params_from_input/_attraction_params_from_input:解析工具入参,分别适配天气/景点工具的参数结构;
工具装饰器 @tool:定义工具描述与入参格式,ReAct 模式下工具入参为 JSON 字符串,需手动解析;
Prompt 模板:严格定义 ReAct 格式(Thought/Action/Action Input/Final Answer),并加入「防重复调用」规则约束。
Agent 初始化:
agent = create_react_agent(
llm=llm,
prompt=prompt_template,
tools=[get_weather, get_attraction],
)
agent_executor = AgentExecutor(
agent=agent,
memory=memory,
tools=[get_weather, get_attraction],
verbose=True,
handle_parsing_errors="返回到上一步并重新尝试,确保Action Input是有效的JSON格式",
max_iterations=8,
early_stopping_method="generate",
stream_runnable=False,
)
2. lc_functioncall.py:FunctionCall 模式
FunctionCall 模式利用 LLM 的工具调用能力,让模型输出结构化的工具调用指令,无需手动解析文本。
关键优化:
- 基于 Pydantic 定义工具入参 Schema(
WeatherInput/AttractionInput),替代 ReAct 模式的 JSON 字符串解析; - 工具函数直接接收结构化参数(
city: str, weather: str),无需手动提取; - Prompt 模板简化:使用
MessagesPlaceholder管理对话历史与 Agent 思考过程,无需手动定义 ReAct 格式。
- 基于 Pydantic 定义工具入参 Schema(
Agent 初始化:
agent = create_tool_calling_agent( llm=llm, prompt=prompt_template, tools=[get_weather, get_attraction], )
3. main.py:FastAPI 服务封装
动态加载 Agent 模式:根据
.env中的AGENT_MODE加载 ReAct 或 FunctionCall 版本的 Agent;接口设计:
/api/chat:接收用户消息,调用 Agent 并返回结果;/api/chat/reset:清空对话记忆;
- 静态资源挂载:将 index.html 作为静态页面挂载,实现前后端一体化部署。
4. index.html:前端交互
- 原生 JS 实现核心逻辑:消息发送 / 接收、加载状态、自适应文本框、对话重置;
- 样式设计:响应式布局(移动端隐藏侧边栏)、气泡式消息展示、加载动画;
- 接口交互:通过
fetch调用后端接口,处理网络错误与服务端异常。
二、开发过程中遇到的问题与解决办法
问题 1:ReAct 模式下 LLM 输出格式不规范
现象
LLM 输出的 Action Input 常拼接无关文本(如 Observation 内容),导致 JSON 解析失败,报错「无法解析 LLM 的输出」。
解决办法
- 编写解析函数:
_json_objects_in_text从杂乱文本中精准提取 JSON 对象,忽略无关拼接内容; - 参数提取优化:
_weather_params_from_input/_attraction_params_from_input针对天气 / 景点工具的参数结构,分别处理 JSON 解析逻辑; - 切换模式:改用 FunctionCall 模式,利用 LLM 原生的工具调用能力,输出结构化参数,从根源避免格式问题。
问题 2:重复调用工具
现象
LLM 可能因「确认天气」「翻译推荐结果」等原因重复调用同一城市的天气 / 景点工具。
解决办法
- 在 Prompt 中加入严格规则:每个城市的
get_weather/get_attraction最多调用 1 次; - 景点推荐结果有效时,强制返回 Final Answer,禁止后续工具调用。
三、ReAct 与 FunctionCall 模式对比
| 维度 | ReAct 模式 | FunctionCall 模式 |
|---|---|---|
| 核心原理 | 基于「思考 - 行动 - 观察」文本循环,手动定义格式并解析 | 利用 LLM 原生工具调用能力,输出结构化指令 |
| 参数处理 | 需手动解析 JSON 字符串,易受格式污染 | 基于 Pydantic 自动校验入参,结构化参数直接使用 |
| 提示词复杂度 | 高:需严格定义 ReAct 格式(Thought/Action 等) | 低:仅需描述工具用途,格式由框架自动处理 |
| 灵活性 | 高:可自定义任意格式的思考过程 | 中:受 LLM 工具调用格式约束 |
| 稳定性 | 低:易因 LLM 输出格式偏差导致解析失败 | 高:结构化输出,解析成功率高 |
| 适用场景 | 复杂推理场景(需显式思考过程) | 简单工具调用场景(参数明确、逻辑固定) |
四、LangChain 对比原生手写 Agent 的优势
1. 工具封装与管理
- LangChain 提供
@tool装饰器,可快速定义工具描述、入参 Schema,原生手写需手动设计工具注册 / 调用逻辑; - 内置工具调用解析器,无需手动处理 LLM 输出的格式校验、参数提取。
2. 记忆能力
- LangChain 提供
ConversationBufferMemory等记忆组件,一键集成对话上下文管理,原生手写需手动维护会话状态; - 记忆组件与 Agent 解耦,可灵活切换记忆策略(如有限窗口记忆、摘要记忆)。
3. Agent 执行框架
AgentExecutor封装了「思考 - 调用工具 - 处理结果」的循环逻辑,原生手写需手动实现循环控制、最大迭代次数、错误处理;- 内置错误处理机制(如
handle_parsing_errors),可快速适配 LLM 输出异常场景。
4. 多模式适配
- 支持 ReAct、FunctionCall、OpenAI Functions 等多种 Agent 模式,切换成本低;
- 原生手写需为每种模式重新设计解析逻辑与执行流程。
5. 生态集成
- 无缝集成 OpenAI/Tavily 等第三方服务,无需手动封装 API 调用;
- 提供 Prompt 模板、输出解析器等组件,降低提示词工程成本。
五、总结
本次项目通过 LangChain 实现了两种模式的旅游助手 Agent,实践发现:
- ReAct 模式适合需要显式推理过程的复杂场景,但需投入大量精力处理格式解析问题;
- FunctionCall 模式更适合简单工具调用场景,稳定性高,开发效率高;
- LangChain 框架大幅降低了 Agent 开发成本,尤其是工具管理、记忆、执行循环等核心模块,相比原生手写可节省 70% 以上的代码量;
- 实际开发中,建议优先使用 FunctionCall 模式(稳定性高),复杂推理场景结合 ReAct 模式的显式思考过程。
后续可优化方向:
- 增加多轮对话的上下文理解能力(如识别「昨天问的上海天气」等指代);
- 接入更多工具(如景点门票、交通信息);
- 优化景点推荐结果,增加本地化 / 个性化策略。
- 添加RAG?虽然暂时没想好怎么个做法,但为了学习是肯定会融入RAG的。
Github连接:https://github.com/KevinJosephDavis/TravelAgent.git