构建智能聊天机器人:Chainlit + LangChain + Tavily 技术实战
在人工智能快速发展的今天,构建一个既具备对话能力又能实时获取网络信息的智能聊天机器人已成为许多开发者的需求。本文将深入解析一个基于现代AI技术栈的聊天机器人项目,该项目巧妙地集成了Chainlit、LangChain和Tavily搜索引擎,实现了流畅的用户交互和强大的信息检索功能。如图是最终效果演示
第一步:搭建舞台 - 导入和配置
import chainlit as cl
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, ToolMessage
from langchain_core.tools import tool
from tavily import TavilyClient
这几行导入就像是在搭建一个舞台,每个库都有自己的角色:
-
Chainlit 是我们的"导演",负责管理整个聊天界面的表演
Chainlit是一个开箱即用的python的ChatUI库,类似于Gradio,streamlit,能够快速搭建AI Chat页面,有一定的自定义程度
-
LangChain 是"编剧",负责协调AI模型和各种工具的对话,就是把各部分串起来
-
Tavily 是我们的"情报员",专门负责到网络上搜集最新信息,Tavily Search API针对RAG(检索增强生成)场景进行了优化,可在单次API调用中完成搜索、抓取、过滤和信息提取,极大地降低了开发者的工作量。搜索结果以结构化方式返回,便于直接传递给LLM作为上下文。
接下来是配置我们的搜索引擎:
#apikey需要自行到官网申请,每月有1000次询问的免费额度
TAVILY_API_KEY = "your_tavily_api_key"
tavily_client = TavilyClient(api_key=TAVILY_API_KEY)
这里创建了一个Tavily客户端,就像是给我们的机器人配备了一副"望远镜",让它能够看到整个互联网的信息海洋。
第二步:打造超级搜索工具 - get_web_data函数
这个函数是整个项目的明珠,让我们逐步剖析:
函数声明和装饰器
@tool
@cl.step(type="tool")
def get_web_data(query: str) -> str:
"""使用Tavily搜索引擎进行网络搜索。
Args:
query: 搜索关键词或问题,例如:"Python教程"、"今日新闻"、"股票价格"等
"""
这两个装饰器就像是给函数贴上了两张"身份证":
@tool
告诉LangChain:“嘿,我是一个可以被AI调用的工具!”@cl.step(type="tool")
告诉Chainlit:“请在界面上显示我的执行过程!”
搜索核心逻辑
response = tavily_client.search(
query,
max_results=3,
search_depth="basic",
include_images=False,
include_answer=True
)
这段代码就像是派出一个非常专业的侦探,给它下达了明确的任务指令:
- “只找3个最相关的结果就够了”(
max_results=3
) - “基础搜索就行,不要太深入”(
search_depth="basic"
) - “不要图片,我们只要文字信息”(
include_images=False
) - “记得给我一个AI总结的答案”(
include_answer=True
)
结果格式化的艺术
这是代码中最精彩的部分,让我们看看它是如何把原始搜索结果变成用户友好的信息卡片的:
formatted_results = []
# 如果有答案摘要,先显示
if 'answer' in response and response['answer']:
formatted_results.append(f"## 📝 AI答案摘要\n\n{response['answer']}\n")
# 添加搜索结果标题
formatted_results.append(f"## 🔍 搜索结果 - {query}\n")
这就像是在设计一份精美的报纸!先放上"头版头条"(AI摘要),然后是醒目的标题。
接下来是更精彩的卡片制作过程:
for i, result in enumerate(results, 1):
title = result.get('title', '无标题')
content = result.get('content', '无内容')
url = result.get('url', '')
card = f"""
### {i}. {title}
{content[:200]}...
🔗 **来源**: [{url}]({url})
---
"""
formatted_results.append(card)
这段代码就像是一个心灵手巧的编辑,为每个搜索结果制作了一张精美的"名片":
- 给每个结果编号,就像杂志的文章目录
- 提取内容的前200个字符,就像制作"内容预览"
- 用Markdown语法创建可点击的链接
- 用分割线把每个结果清晰地分开
错误处理的智慧
except Exception as e:
error_msg = str(e)
if "quota" in error_msg.lower() or "limit" in error_msg.lower():
return f"⚠️ 搜索API配额已用完。关于 '{query}',请稍后重试或直接提问,我会基于现有知识尽力回答。"
elif "unauthorized" in error_msg.lower():
return f"🔑 搜索API密钥无效。请检查配置后重试。"
else:
return f"❌ 搜索时发生错误: {error_msg}。请尝试重新搜索或换个关键词。"
这段错误处理代码就像是一个经验丰富的客服代表,能够根据不同的错误类型给出贴心的解决建议:
- 看到"quota"或"limit"?马上知道是配额用完了
- 看到"unauthorized"?立刻明白是认证问题
- 其他错误?也会给出通用但有用的建议
第三步:大脑中枢 - AI模型配置
api_key = "your_API_key"
base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1"
model_name = "qwen3-32b"
llm = ChatOpenAI(
api_key=api_key,
base_url=base_url,
model=model_name,
extra_body={"enable_thinking": False}
)
llm_with_tools = llm.bind_tools(tools)
这段代码就像是在组装一个超级大脑:
- 首先连接到阿里云的通义千问模型,这是我们的"CPU"
extra_body={"enable_thinking": False}
是一个很聪明的设置,告诉模型"不要显示你的思考过程,直接给我答案就行",也可以开启思考模式,但前端显示也要做相应配置bind_tools(tools)
就像是给大脑装上了"手臂",让它能够使用我们刚才定义的搜索工具
第四步:事件驱动的生命周期
聊天开始
@cl.on_chat_start
def on_chat_start():
print("A new chat session has started!")
这就像是餐厅的"欢迎光临",简单但重要。虽然现在只是打印一条消息,但你可以在这里做很多初始化工作。
消息处理的核心逻辑
@cl.on_message
async def on_message(message: cl.Message):
response = cl.Message(content="")
messages = [HumanMessage(message.content)]
每当用户发送消息,这个函数就像是一个智能接待员,立刻开始工作:
- 创建一个空的回复消息,准备装载内容
- 把用户的消息包装成LangChain能理解的格式
流式响应的魔法
ai_msg = None
for chunk in llm_with_tools.stream(messages):
if chunk.tool_calls:
ai_msg = chunk
break
await response.stream_token(chunk.content)
if ai_msg is None:
await response.send()
return
流式响应以优化用户体验,这点就多说了,分chunk输出即可
- 边听边回应:如果AI直接回答(不需要搜索),就实时显示每个生成的词汇
- 识别特殊需求:一旦发现AI想要调用工具(比如搜索),立刻停止显示,准备执行特殊服务
- 灵活处理:如果没有工具调用,就直接发送回复
工具调用的协调过程
print(ai_msg.tool_calls)
messages.append(ai_msg)
for tool_call in ai_msg.tool_calls:
tool_name = tool_call["name"]
print(f"Processing tool call: {tool_name}")
一旦AI决定使用工具,代码就进入了"特殊服务模式",像一个高效的调度员开始协调工作。
智能参数处理
args = tool_call.get("args", {})
if not args.get("query"):
args = {"query": message.content}
print(f"Using default query: {message.content}")
这段代码展现了程序的"智慧",就像一个善解人意的助手:
- 如果AI没有指定搜索关键词,就聪明地使用用户的原始问题
- 这避免了因为参数缺失而导致的搜索失败
工具执行和结果处理
try:
print(f"Invoking tool with args: {args}")
tool_result = selected_tool.invoke(args)
print(f"Tool result length: {len(str(tool_result))}")
except Exception as e:
print(f"Error in tool execution: {e}")
tool_result = f"工具执行出错: {str(e)}"
try:
tool_msg = ToolMessage(content=str(tool_result), tool_call_id=tool_call["id"])
messages.append(tool_msg)
print(f"Successfully created ToolMessage")
这段代码就像是一个经验丰富的项目经理,不仅执行任务,还要:
- 记录执行过程(各种print语句)
- 处理可能的意外情况
- 确保结果正确地传递给下一个环节
最终回答的生成
print("Generating final response...")
final_response = cl.Message(content="")
try:
for token in llm_with_tools.stream(messages):
await final_response.stream_token(token.content)
await final_response.send()
print("Final response sent successfully")
最后这段代码就像是把所有信息(用户问题、AI的工具调用、搜索结果)打包送给AI,让它生成一个综合性的回答。整个过程依然使用流式输出,确保用户能够实时看到回答的生成过程。
第五步:优雅的收尾
@cl.on_stop
def on_stop():
print("The user wants to stop the task!")
@cl.on_chat_end
def on_chat_end():
print("The user disconnected!")
这两个函数就像是礼貌的告别,确保系统能够优雅地处理用户的离开。当然这只是终端的输出
总结:一个完整的AI生态系统
这段代码虽然只有100多行,但它构建了一个完整的AI生态系统:
- 前端: Chainlit提供美观的聊天界面
- 中间件: LangChain协调各组件的交互
- 后端: 通义千问提供AI能力
- 外部服务: Tavily提供实时信息检索
最后附上完整版代码
import chainlit as cl
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, ToolMessage
from langchain_core.tools import tool
from tavily import TavilyClient
# Tavily API配置
TAVILY_API_KEY = "your_api_key"
tavily_client = TavilyClient(api_key=TAVILY_API_KEY)
@tool
@cl.step(type="tool")
def get_web_data(query: str) -> str:
"""使用Tavily搜索引擎进行网络搜索。
Args:
query: 搜索关键词或问题,例如:"Python教程"、"今日新闻"、"股票价格"等
"""
try:
# 使用Tavily进行搜索
response = tavily_client.search(
query,
max_results=3,
search_depth="basic",
include_images=False,
include_answer=True
)
if response and 'results' in response:
results = response['results']
# 创建格式化的markdown结果
formatted_results = []
# 如果有答案摘要,先显示
if 'answer' in response and response['answer']:
formatted_results.append(f"## 📝 AI答案摘要\n\n{response['answer']}\n")
# 添加搜索结果标题
formatted_results.append(f"## 🔍 搜索结果 - {query}\n")
# 格式化搜索结果为卡片样式
for i, result in enumerate(results, 1):
title = result.get('title', '无标题')
content = result.get('content', '无内容')
url = result.get('url', '')
card = f"""
### {i}. {title}
{content[:200]}...
🔗 **来源**: [{url}]({url})
---
"""
formatted_results.append(card)
return "\n".join(formatted_results)
else:
return f"❌ 抱歉,没有找到关于 '{query}' 的搜索结果。"
except Exception as e:
error_msg = str(e)
if "quota" in error_msg.lower() or "limit" in error_msg.lower():
return f"⚠️ 搜索API配额已用完。关于 '{query}',请稍后重试或直接提问,我会基于现有知识尽力回答。"
elif "unauthorized" in error_msg.lower():
return f"🔑 搜索API密钥无效。请检查配置后重试。"
else:
return f"❌ 搜索时发生错误: {error_msg}。请尝试重新搜索或换个关键词。"
tools = [get_web_data]
api_key = "your_api_key"
base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1"
model_name = "qwen3-32b"
# 正确创建ChatOpenAI对象
llm = ChatOpenAI(
api_key=api_key,
base_url=base_url,
model=model_name,
extra_body={"enable_thinking": False}
)
llm_with_tools = llm.bind_tools(tools)
@cl.on_chat_start
def on_chat_start():
print("A new chat session has started!")
# Set the assistant agent in the user session.
@cl.on_message
async def on_message(message: cl.Message):
response = cl.Message(content="")
messages = [HumanMessage(message.content)]
# 使用流式调用来避免enable_thinking参数问题
ai_msg = None
for chunk in llm_with_tools.stream(messages):
if chunk.tool_calls:
ai_msg = chunk
break
await response.stream_token(chunk.content)
if ai_msg is None:
# 如果没有工具调用,直接返回响应
await response.send()
return
print(ai_msg.tool_calls)
messages.append(ai_msg)
# 处理工具调用
for tool_call in ai_msg.tool_calls:
tool_name = tool_call["name"]
print(f"Processing tool call: {tool_name}")
try:
# 检查工具是否存在
if tool_name not in {"get_web_data"}:
print(f"Unknown tool: {tool_name}")
error_msg = ToolMessage(content=f"未知工具: {tool_name}", tool_call_id=tool_call["id"])
messages.append(error_msg)
continue
selected_tool = get_web_data
# 处理参数缺失的情况
args = tool_call.get("args", {})
print(f"Tool: {tool_name}, Args: {args}")
# 为搜索工具添加默认参数处理
if not args.get("query"):
args = {"query": message.content}
print(f"Using default query: {message.content}")
except Exception as e:
print(f"Error in tool setup: {e}")
error_msg = ToolMessage(content=f"工具设置错误: {str(e)}", tool_call_id=tool_call["id"])
messages.append(error_msg)
continue
try:
# 调用工具
print(f"Invoking tool with args: {args}")
tool_result = selected_tool.invoke(args)
print(f"Tool result length: {len(str(tool_result))}")
except Exception as e:
print(f"Error in tool execution: {e}")
tool_result = f"工具执行出错: {str(e)}"
try:
# 创建ToolMessage对象
tool_msg = ToolMessage(content=str(tool_result), tool_call_id=tool_call["id"])
messages.append(tool_msg)
print(f"Successfully created ToolMessage")
except Exception as e:
print(f"Error creating ToolMessage: {e}")
# 创建简化的错误消息
error_msg = ToolMessage(content="工具消息创建失败", tool_call_id=tool_call["id"])
messages.append(error_msg)
# 生成最终回答
print("Generating final response...")
final_response = cl.Message(content="")
try:
for token in llm_with_tools.stream(messages):
await final_response.stream_token(token.content)
await final_response.send()
print("Final response sent successfully")
except Exception as e:
print(f"Error in final response generation: {e}")
# 发送错误消息
error_response = cl.Message(content=f"生成回答时出错: {str(e)}")
await error_response.send()
@cl.on_stop
def on_stop():
print("The user wants to stop the task!")
@cl.on_chat_end
def on_chat_end():
print("The user disconnected!")