langchain+tavily+flask实现智能联网问答系统

概述
时序图(豆包生成):

html页面展示:

在 AI 应用爆发的当下,搭建一个兼具实用性与可扩展性的智能聊天系统,成为许多开发者的实践目标。本文将以一个完整的全栈聊天项目为例,从后端架构设计、工具层实现到前端交互开发,分步骤拆解系统的核心实现逻辑,并通过流程图直观呈现各模块的协作关系,帮助开发者快速掌握全栈 AI 应用的构建思路。
一、项目整体架构:模块化设计的 “分层协作” 模式
项目的核心文件结构如下,各文件对应不同层级的功能:
-
前端层:
index.html(负责用户交互界面与前端逻辑) -
后端入口层:
run.py(Flask 应用初始化与配置) -
API 接口层:
api.py(聊天接口实现与请求处理) -
工具核心层:
tools.py(LLM 模型、搜索工具与 Agent 构建) -
配置层:
config.yaml(集中管理模型参数与第三方密钥)
简单说,用户在前端输入消息后,请求经后端接口层传递到工具层,由 Agent 调用模型或搜索工具生成回复,再经接口层返回前端展示。
各层可独立开发(前端无需关注后端模型细节,后端无需修改前端样式)
二、后端实现
后端是系统的 “能力中枢”,负责接收前端请求、调用 AI 模型并返回结果。
应用入口(run.py):Flask 应用的 “启动器”
run.py是项目的启动入口,主要完成 Flask 应用的初始化、蓝图注册与运行配置,很简单的几行代码:
from flask import Flask
from api import bp
app = Flask(__name__,static_folder='.', static_url_path='')
app.register_blueprint(bp, url_prefix="/api")
if __name__ == "__main__":
app.run(host="0.0.0.0",port=5000,debug= True)
API 接口层(api.py):请求处理的 “中转站”
api.py通过 Flask 蓝图实现聊天接口,负责接收前端请求、校验参数、调用 Agent 并返回结果,核心流程如下:
步骤 1:创建蓝图与初始化 Agent
先定义蓝图bp(名称为 “chat”),再通过tools.build_agent()得到一个实例化的 agetn:
from flask import Blueprint, request, jsonify
from tools import build_agent
bp = Blueprint("chat", __name__)
agent = build_agent() # 初始化Agent
步骤 2:定义聊天接口(/chat)
然后是接口定义,支持POST方式请求
@bp.route("/chat", methods=["POST"])
接口逻辑分为 “参数校验”“Agent 调用”“结果返回” 三部分:
参数校验:通过request.get_json()获取前端传入的 JSON 数据,提取message字段,若为空则返回 400 错误:
"""
请求体 JSON:
{
"message": "用户问题"
}
返回:
{
"response": "...",
"success": true
}
"""
data = request.get_json(silent=True) or {}
user_msg = data.get("message", "").strip()
if not user_msg:
return jsonify({"error": "message 不能为空", "success": False}), 400
调用 Agent 获取回复:通过agent.run(user_msg)调用 Agent 处理用户问题,获取 AI 回复;
异常处理与结果返回:使用try-except捕获异常(如模型调用失败),正常情况返回response(AI 回复),异常情况返回错误信息与 500 状态码:
try:
answer = agent.run(user_msg)
return jsonify({"response": answer, "success": True})
except Exception as e:
return jsonify({"error": str(e), "success": False}), 500
工具层与配置层
config.yaml集中管理模型与工具的参数,tools.py则基于这些配置构建 LLM 模型、搜索工具与智能 Agent。
配置层(config.yaml)
本地大模型配置(Ollama/LM Studio)
支持两种本地部署工具(Ollama 与 LM Studio),配置项包括服务地址(base_url)、模型名称(model)、生成参数(temperature控制随机性,max_tokens控制回复长度):
ollama:
base_url: "" # Ollama服务地址
model: "" # 加载的模型
temperature: 0.7 # 随机性(0-1)
max_tokens: 2048 # 最大回复长度
第三方大模型配置(Kimi)
若需使用外部 API(如 Kimi),则配置其接口地址(base_url)、API 密钥(api_key)与模型名称(model_name),现在很多商家都是兼容openai格式的:
kimi:
base_url: "https://api.moonshot.cn/v1"
api_key: ""
model_name: ""
搜索工具配置(Tavily)
配置 Tavily 搜索的 API 密钥(api_key)与最大搜索结果数(max_results),用于 Agent 联网获取实时信息:
tavily:
api_key: ""
max_results: 3 # 每次搜索返回3条结果
工具层(tools.py)
构建 LLM 模型(支持多类型模型)
通过build_llm()函数,根据PRIMARY变量(可切换为ollama/kimi/lmstudio)选择模型类型,读取对应配置并初始化模型:
def build_llm():
if PRIMARY == "ollama":
return ChatOllama(
base_url=cfg["ollama"]["base_url"],
model=cfg["ollama"]["model"],
temperature=cfg["ollama"]["temperature"],
max_tokens=cfg["ollama"]["max_tokens"]
)
elif PRIMARY == "kimi":
kimi_models = OpenAI(
openai_api_base=cfg["kimi"]["base_url"],
openai_api_key=cfg["kimi"]["api_key"],
model_name=cfg["kimi"]["model_name"],
temperature=cfg["kimi"]["temperature"],
max_tokens=cfg["kimi"]["max_tokens"]
)
return kimi_models
else: # lmstudio
return OpenAI(
openai_api_base=cfg["lmstudio"]["base_url"],
openai_api_key="lm-studio",
model_name=cfg["lmstudio"]["model"],
temperature=cfg["lmstudio"]["temperature"],
max_tokens=cfg["lmstudio"]["max_tokens"]
)
构建 Agent(串联模型与工具)
Agent 是 “决策者”—— 它能根据用户问题判断是否需要调用工具(如搜索),再结合模型生成最终回复。通过initialize_agent()函数初始化 Agent,传入 “工具列表”“LLM 模型”“Agent 类型” 等参数:
def build_agent():
search_tool = build_search_tool()
llm = build_llm()
tools = [
Tool(
name="TavilySearch",
func=search_tool.run,
description="当问题涉及2023年之后的新闻、数据、事件动态(如游戏发布日期、政策变化、科技进展等),需要联网搜索获取最新信息时使用。"
)
]
agent = initialize_agent(
tools=tools,
llm=llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True,
handle_parsing_errors=True
)
return agent
四、前端实现(豆包)
页面采用 “顶部操作栏 + 聊天容器 + 输入区” 的经典布局
就直接放下面了
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>one</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<!-- 外部样式引入 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
:root {
/* 浅色主题变量 */
--bg-chat: #ffffff;
--bg-user: #e3f2fd;
--bg-agent: #e8f5e9;
--text-user: #0d47a1;
--text-agent: #1b5e20;
--bg-chat-box: #f8f9fa;
--text-timestamp: #888;
}
[data-theme="dark"] {
/* 深色主题变量 */
--bg-chat: #1e1e1e;
--bg-user: #0d47a1;
--bg-agent: #2e7d32;
--text-user: #fff;
--text-agent: #fff;
--bg-chat-box: #2d2d2d;
--text-timestamp: #ccc;
}
body {
background: var(--bg-chat);
transition: background 0.3s;
}
#chatBox {
height: 75vh;
overflow-y: auto;
scroll-behavior: smooth;
background: var(--bg-chat-box);
transition: background 0.3s;
}
.msg {
max-width: 70%;
word-break: break-word;
animation: fadeIn 0.3s;
transition: background 0.3s, color 0.3s;
}
@keyframes fadeIn {
from {opacity: 0; transform: translateY(10px);}
to {opacity: 1; transform: none;}
}
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
object-fit: cover;
}
.timestamp {
font-size: 0.7rem;
color: var(--text-timestamp);
transition: color 0.3s;
}
.type-cursor::after {
content: "▋";
animation: blink 1s infinite;
}
@keyframes blink {
50% { opacity: 0; }
}
/* 新增:统一头像与气泡的间距样式 */
.avatar-gap {
margin: 0 8px; /* 左右间距8px,与AI保持一致 */
}
</style>
</head>
<body class="container-fluid p-3">
<!-- 顶部操作栏 -->
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0">ONE</h4>
<div>
<button class="btn btn-sm btn-outline-secondary me-2" onclick="toggleTheme()">
<i id="themeIcon" class="fas fa-moon"></i>
</button>
<button class="btn btn-sm btn-outline-success me-2" onclick="exportChat()">
<i class="fas fa-download"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="clearChat()">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<!-- 聊天记录容器 -->
<div id="chatBox" class="border rounded p-3" data-theme="light"></div>
<!-- 消息输入区 -->
<div class="input-group mt-3">
<input id="msgInput" class="form-control" placeholder="请输入问题…" />
<button id="sendBtn" class="btn btn-primary">
<i class="fas fa-paper-plane"></i> 发送
</button>
</div>
<script>
// ---------- 主题切换功能 ----------
function toggleTheme() {
const html = document.documentElement;
const isDark = html.getAttribute('data-theme') === 'dark';
html.setAttribute('data-theme', isDark ? 'light' : 'dark');
document.getElementById('themeIcon').className = isDark ? 'fas fa-moon' : 'fas fa-sun';
updateAllBubbleTheme();
}
function updateAllBubbleTheme() {
const chatBox = document.getElementById('chatBox');
const userBubbles = chatBox.querySelectorAll('.msg[data-role="user"]');
const agentBubbles = chatBox.querySelectorAll('.msg[data-role="agent"]');
userBubbles.forEach(bubble => {
bubble.style.background = getComputedStyle(document.documentElement).getPropertyValue('--bg-user');
bubble.style.color = getComputedStyle(document.documentElement).getPropertyValue('--text-user');
});
agentBubbles.forEach(bubble => {
bubble.style.background = getComputedStyle(document.documentElement).getPropertyValue('--bg-agent');
bubble.style.color = getComputedStyle(document.documentElement).getPropertyValue('--text-agent');
});
}
// ---------- 聊天记录本地存储功能 ----------
const STORAGE_KEY = 'chat_history';
function loadHistory() {
const history = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
history.forEach(item => renderBubble(item));
updateAllBubbleTheme();
}
function saveBubble(role, text) {
const history = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
history.push({
role: role,
text: text,
ts: Date.now()
});
localStorage.setItem(STORAGE_KEY, JSON.stringify(history));
}
function clearChat() {
localStorage.removeItem(STORAGE_KEY);
document.getElementById('chatBox').innerHTML = '';
}
function exportChat() {
const history = localStorage.getItem(STORAGE_KEY) || '[]';
const blob = new Blob([history], { type: 'application/json; charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `chat_history_${new Date().toLocaleDateString()}.json`;
a.click();
URL.revokeObjectURL(url);
}
// ---------- 消息渲染功能(核心:调整头像间距) ----------
function renderBubble({ role, text, ts }, animate = false) {
const chatBox = document.getElementById('chatBox');
const msgContainer = document.createElement('div');
// 角色对齐:用户靠右,AI靠左
msgContainer.className = `d-flex ${role === 'user' ? 'justify-content-end' : 'justify-content-start'} mb-3`;
const timeStr = new Date(ts).toLocaleTimeString();
// 头像位置与间距逻辑:
// - 统一使用 avatar-gap 类控制间距(8px)
// - 通过 order 控制显示顺序,实现用户头像在右、AI头像在左
let avatarHtml = '';
let bubbleWrapperClass = '';
if (role === 'user') {
// 用户消息:头像在气泡右侧,间距通过 avatar-gap 控制
avatarHtml = `<img class="avatar avatar-gap order-1" src="/static/img/white_dog.png" alt="用户头像">`;
bubbleWrapperClass = 'order-0'; // 气泡排在头像前面(视觉上头像在右)
} else {
// AI消息:头像在气泡左侧,间距通过 avatar-gap 控制
avatarHtml = `<img class="avatar avatar-gap order-0" src="/static/img/black_dog.png" alt="AI头像">`;
bubbleWrapperClass = 'order-1'; // 气泡排在头像后面(视觉上头像在左)
}
msgContainer.innerHTML = `
<div class="d-flex align-items-start">
${avatarHtml}
<div class="${bubbleWrapperClass}">
<div class="msg p-2 rounded"
data-role="${role}"
style="background: var(--bg-${role}); color: var(--text-${role});">
${animate ? `<span class="type-cursor">${text}</span>` : text}
<div class="timestamp mt-1 text-end">${timeStr}</div>
</div>
</div>
</div>
`;
chatBox.appendChild(msgContainer);
chatBox.scrollTop = chatBox.scrollHeight;
}
// ---------- 打字机效果功能 ----------
async function typeText(element, text) {
element.textContent = '';
for (let i = 0; i < text.length; i++) {
element.textContent = text.slice(0, i + 1);
await new Promise(resolve => setTimeout(resolve, 25));
}
element.classList.remove('type-cursor');
}
// ---------- 发送消息功能 ----------
async function sendMessage() {
const input = document.getElementById('msgInput');
const userText = input.value.trim();
if (!userText) return;
renderBubble({ role: 'user', text: userText, ts: Date.now() });
saveBubble('user', userText);
input.value = '';
const loadingTs = Date.now();
renderBubble({ role: 'agent', text: '▍思考中…', ts: loadingTs }, true);
const aiMsgElement = document.querySelector('#chatBox .msg:last-child span');
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: userText })
});
const result = await response.json();
const aiReply = result.response || result.error || '抱歉,暂时无法获取回复';
await typeText(aiMsgElement, aiReply);
saveBubble('agent', aiReply);
} catch (error) {
aiMsgElement.textContent = `网络错误:${error.message}`;
aiMsgElement.classList.remove('type-cursor');
saveBubble('agent', `网络错误:${error.message}`);
}
}
// ---------- 事件绑定 ----------
document.getElementById('sendBtn').addEventListener('click', sendMessage);
document.getElementById('msgInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// ---------- 页面初始化 ----------
window.onload = function() {
loadHistory();
};
</script>
</body>
</html>
更多推荐


所有评论(0)