> 零基础搭建一个带可视化界面的AI聊天机器人,从环境搭建到前后端联调,完整走一遍。

前言

大语言模型(LLM)的火热让"AI聊天机器人"成了每个开发者都想动手试试的项目。但很多人卡在了第一步——怎么把模型能力接到自己的代码里?

这篇文章带你从零搭建一个完整的聊天机器人应用:

- **后端**:Python + Flask + LangChain + 通义千问

- **前端**:纯 HTML/CSS/JS,无框架依赖

- **通信**:REST API + Axios

最终效果:浏览器打开页面,打字发送,AI实时回复,支持多轮对话。

一、环境准备

1.1 Python 版本

本项目使用 **Python 3.13**,建议 3.10 以上版本均可。

1.2 安装依赖

pip install langchain                # LangChain 核心框架
pip install langchain-community      # 第三方模型集成
pip install langchain-openai         # OpenAI 兼容接口(通义千问用这个)
pip install python-dotenv            # 环境变量管理
pip install flask                    # Web 后端框架
pip install flask-cors               # 跨域支持

1.3 获取通义千问 API Key

通义千问是阿里云推出的大语言模型,提供了兼容 OpenAI 的 API 接口,对国内开发者非常友好。

1. 访问 [阿里云 DashScope 控制台](https://dashscope.console.aliyun.com/)

2. 注册/登录阿里云账号

3. 开通 DashScope 服务(新用户有免费额度)

4. 在「API-KEY 管理」中创建一个新的 Key

拿到 Key 后,在项目根目录创建 `.env` 文件:

DASHSCOPE_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx

为什么用 .env 文件?** 把密钥写在代码里是危险的,一旦上传到 Git 就泄露了。`.env` 文件可以加入 `.gitignore`,保证密钥安全。

二、核心原理:LangChain + 通义千问

2.1 为什么选 LangChain?

LangChain 是目前最流行的 LLM 应用开发框架。它的核心价值在于:

- **统一接口**:同一套代码,换模型只需改两行配置

- **消息管理**:自动管理对话历史(System / Human / AI 消息)

- **生态丰富**:链式调用、Agent、RAG 等高级能力随时可扩展

2.2 通义千问的接入方式

通义千问提供了 **OpenAI 兼容模式**,这意味着我们不需要学新的 SDK,直接用 LangChain 的 `ChatOpenAI` 类,把接口地址换成阿里云的就行:

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    model="qwen-plus",                                    # 模型名称
    openai_api_key="你的API Key",
    openai_api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
)

就这么简单,三行代码接入大模型。

2.3 可选模型

| 模型 | 特点 | 适用场景 |

| `qwen-turbo` | 速度快,成本低 | 简单问答、快速响应 |

| `qwen-plus` | 均衡型 | 通用对话(推荐) |

| `qwen-max` | 能力最强 | 复杂推理、长文本 |

三、命令行版:最小可运行Demo

先把最核心的聊天逻辑跑通,文件名 `chat_qwen.py`:

import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage

# 加载 .env 中的环境变量
load_dotenv()


def create_chat_model():
    """创建通义千问聊天模型"""
    return ChatOpenAI(
        model="qwen-plus",
        openai_api_key=os.getenv("DASHSCOPE_API_KEY"),
        openai_api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
    )


def main():
    chat = create_chat_model()

    # 初始化对话历史,设定 AI 的角色
    messages = [
        SystemMessage(content="你是一个有用的AI助手,请用中文回答问题。")
    ]

    print("通义千问聊天机器人 (输入 quit 退出)")
    print("=" * 40)

    while True:
        user_input = input("\n你: ").strip()
        if not user_input:
            continue
        if user_input.lower() == "quit":
            print("再见!")
            break

        # 把用户消息加入历史
        messages.append(HumanMessage(content=user_input))

        # 调用模型(传入完整历史,实现多轮对话)
        response = chat.invoke(messages)

        # 把 AI 回复也加入历史,下一轮它会"记住"
        messages.append(AIMessage(content=response.content))

        print(f"\nAI: {response.content}")


if __name__ == "__main__":
    main()

运行:

python chat_qwen.py

到这一步,已经完成了核心功能——一个支持多轮对话的命令行聊天机器人。

关键点解析:

- `SystemMessage`:设定 AI 的角色和行为准则,每次对话都会带上

- `HumanMessage`:用户说的话

- `AIMessage`:AI 的回复

- 每次调用 `chat.invoke(messages)` 时传入“完整的消息列表”,模型据此理解上下文

四、Web化:从命令行到可视化界面

4.1 整体架构

浏览器 (index.html)

    │  axios POST /api/chat

Flask 后端 (server.py)

    │  LangChain 调用通义千问 API

阿里云 DashScope API

    │

通义千问大模型 → 返回结果

4.2 后端 API:server.py

import os
import uuid
from dotenv import load_dotenv
from flask import Flask, request, jsonify, send_from_directory
from flask_cors import CORS
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage

load_dotenv()

app = Flask(__name__, static_folder="static")
CORS(app)

# 用字典在内存中存储各会话的聊天历史
sessions = {}
SYSTEM_PROMPT = "你是一个有用的AI助手,请用中文回答问题。"


def get_chat_model():
    return ChatOpenAI(
        model="qwen-plus",
        openai_api_key=os.getenv("DASHSCOPE_API_KEY"),
        openai_api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
    )


# 前端页面
@app.route("/")
def index():
    return send_from_directory("static", "index.html")


# 聊天接口
@app.route("/api/chat", methods=["POST"])
def chat():
    data = request.json
    user_message = data.get("message", "").strip()
    session_id = data.get("session_id")

    if not user_message:
        return jsonify({"error": "消息不能为空"}), 400

    # 首次访问,创建新会话
    if not session_id or session_id not in sessions:
        session_id = str(uuid.uuid4())
        sessions[session_id] = [SystemMessage(content=SYSTEM_PROMPT)]

    history = sessions[session_id]
    history.append(HumanMessage(content=user_message))

    try:
        llm = get_chat_model()
        response = llm.invoke(history)
        history.append(AIMessage(content=response.content))

        return jsonify({
            "reply": response.content,
            "session_id": session_id,
        })
    except Exception as e:
        return jsonify({"error": str(e)}), 500


# 新建会话接口
@app.route("/api/new-session", methods=["POST"])
def new_session():
    session_id = str(uuid.uuid4())
    sessions[session_id] = [SystemMessage(content=SYSTEM_PROMPT)]
    return jsonify({"session_id": session_id})


if __name__ == "__main__":
    print("聊天机器人服务启动: http://127.0.0.1:5000")
    app.run(debug=True, port=5000)

API 说明:

| 接口 | 方法 | 功能 | 参数 |

| `/api/chat` | POST | 发送消息并获取AI回复 | `{ message, session_id }` |

| `/api/new-session` | POST | 创建新的对话会话 | 无 |

| `/` | GET | 返回前端页面 | 无 |

会话管理思路:用 `session_id` 区分不同对话,每个 session 维护独立的消息历史。实际生产环境可替换为 Redis 或数据库存储。

4.3 前端页面:static/index.html

前端是纯 HTML + CSS + JS 的单文件方案,引入了两个 CDN 库:

- Axios:HTTP 请求

- Marked.js:将 AI 回复中的 Markdown 渲染为 HTML(支持代码块高亮)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>通义千问聊天机器人</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }

        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: #f0f2f5;
            height: 100vh;
            display: flex;
            flex-direction: column;
        }

        /* 顶部栏 */
        .header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: #fff;
            padding: 16px 24px;
            display: flex;
            align-items: center;
            justify-content: space-between;
            box-shadow: 0 2px 8px rgba(0,0,0,0.15);
        }
        .header h1 { font-size: 20px; font-weight: 600; }
        .header button {
            background: rgba(255,255,255,0.2);
            border: 1px solid rgba(255,255,255,0.4);
            color: #fff;
            padding: 6px 16px;
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
            transition: background 0.2s;
        }
        .header button:hover { background: rgba(255,255,255,0.35); }

        /* 聊天区域 */
        .chat-container {
            flex: 1;
            overflow-y: auto;
            padding: 20px;
            max-width: 800px;
            width: 100%;
            margin: 0 auto;
        }

        .message {
            display: flex;
            margin-bottom: 16px;
            animation: fadeIn 0.3s ease;
        }
        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(8px); }
            to { opacity: 1; transform: translateY(0); }
        }

        .message.user { justify-content: flex-end; }
        .message.ai { justify-content: flex-start; }

        .avatar {
            width: 36px;
            height: 36px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 16px;
            flex-shrink: 0;
        }
        .message.user .avatar { background: #667eea; color: #fff; margin-left: 10px; order: 2; }
        .message.ai .avatar { background: #e8e8e8; color: #555; margin-right: 10px; }

        .bubble {
            max-width: 70%;
            padding: 12px 16px;
            border-radius: 12px;
            line-height: 1.6;
            font-size: 15px;
            word-break: break-word;
        }
        .message.user .bubble {
            background: linear-gradient(135deg, #667eea, #764ba2);
            color: #fff;
            border-bottom-right-radius: 4px;
        }
        .message.ai .bubble {
            background: #fff;
            color: #333;
            border-bottom-left-radius: 4px;
            box-shadow: 0 1px 4px rgba(0,0,0,0.08);
        }
        .message.ai .bubble p { margin: 0.4em 0; }
        .message.ai .bubble pre {
            background: #1e1e1e;
            color: #d4d4d4;
            padding: 12px;
            border-radius: 6px;
            overflow-x: auto;
            margin: 8px 0;
            font-size: 13px;
        }
        .message.ai .bubble code {
            background: #f0f0f0;
            padding: 2px 6px;
            border-radius: 3px;
            font-size: 13px;
        }
        .message.ai .bubble pre code { background: none; padding: 0; }

        /* 加载动画 */
        .typing-indicator {
            display: flex;
            gap: 4px;
            padding: 4px 0;
        }
        .typing-indicator span {
            width: 8px; height: 8px;
            background: #999;
            border-radius: 50%;
            animation: bounce 1.4s infinite;
        }
        .typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
        .typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
        @keyframes bounce {
            0%, 60%, 100% { transform: translateY(0); }
            30% { transform: translateY(-6px); }
        }

        /* 输入区域 */
        .input-area {
            background: #fff;
            padding: 16px 20px;
            border-top: 1px solid #e0e0e0;
            box-shadow: 0 -2px 8px rgba(0,0,0,0.05);
        }
        .input-wrapper {
            max-width: 800px;
            margin: 0 auto;
            display: flex;
            gap: 12px;
        }
        .input-wrapper textarea {
            flex: 1;
            padding: 12px 16px;
            border: 1px solid #ddd;
            border-radius: 10px;
            font-size: 15px;
            resize: none;
            outline: none;
            font-family: inherit;
            transition: border-color 0.2s;
            min-height: 46px;
            max-height: 120px;
        }
        .input-wrapper textarea:focus { border-color: #667eea; }

        .input-wrapper button {
            background: linear-gradient(135deg, #667eea, #764ba2);
            color: #fff;
            border: none;
            padding: 12px 24px;
            border-radius: 10px;
            font-size: 15px;
            cursor: pointer;
            transition: opacity 0.2s;
            white-space: nowrap;
        }
        .input-wrapper button:hover { opacity: 0.9; }
        .input-wrapper button:disabled { opacity: 0.5; cursor: not-allowed; }

        .error-msg {
            color: #e74c3c;
            text-align: center;
            padding: 8px;
            font-size: 14px;
        }
    </style>
</head>
<body>

<div class="header">
    <h1>通义千问聊天机器人</h1>
    <button onclick="newSession()">新对话</button>
</div>

<div class="chat-container" id="chatContainer">
    <div class="message ai">
        <div class="avatar">AI</div>
        <div class="bubble">你好!我是通义千问助手,有什么可以帮你的吗?</div>
    </div>
</div>

<div class="input-area">
    <div class="input-wrapper">
        <textarea id="userInput" placeholder="输入消息,按 Enter 发送..." rows="1"
                  oninput="autoResize(this)"></textarea>
        <button id="sendBtn" onclick="sendMessage()">发送</button>
    </div>
</div>

<script>
    let sessionId = null;
    const container = document.getElementById('chatContainer');
    const userInput = document.getElementById('userInput');
    const sendBtn = document.getElementById('sendBtn');

    // Enter 发送, Shift+Enter 换行
    userInput.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' && !e.shiftKey) {
            e.preventDefault();
            sendMessage();
        }
    });

    function autoResize(el) {
        el.style.height = 'auto';
        el.style.height = Math.min(el.scrollHeight, 120) + 'px';
    }

    function scrollToBottom() {
        container.scrollTop = container.scrollHeight;
    }

    function addMessage(role, content) {
        const div = document.createElement('div');
        div.className = `message ${role}`;

        const avatar = document.createElement('div');
        avatar.className = 'avatar';
        avatar.textContent = role === 'user' ? '我' : 'AI';

        const bubble = document.createElement('div');
        bubble.className = 'bubble';

        if (role === 'ai') {
            bubble.innerHTML = marked.parse(content);
        } else {
            bubble.textContent = content;
        }

        div.appendChild(avatar);
        div.appendChild(bubble);
        container.appendChild(div);
        scrollToBottom();
        return bubble;
    }

    function addTypingIndicator() {
        const div = document.createElement('div');
        div.className = 'message ai';
        div.id = 'typing';

        const avatar = document.createElement('div');
        avatar.className = 'avatar';
        avatar.textContent = 'AI';

        const bubble = document.createElement('div');
        bubble.className = 'bubble';
        bubble.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';

        div.appendChild(avatar);
        div.appendChild(bubble);
        container.appendChild(div);
        scrollToBottom();
    }

    function removeTypingIndicator() {
        const el = document.getElementById('typing');
        if (el) el.remove();
    }

    async function sendMessage() {
        const message = userInput.value.trim();
        if (!message) return;

        userInput.value = '';
        userInput.style.height = 'auto';
        sendBtn.disabled = true;

        addMessage('user', message);
        addTypingIndicator();

        try {
            const res = await axios.post('/api/chat', { message, session_id: sessionId });
            removeTypingIndicator();
            sessionId = res.data.session_id;
            addMessage('ai', res.data.reply);
        } catch (err) {
            removeTypingIndicator();
            const errMsg = err.response?.data?.error || '请求失败,请重试';
            addMessage('ai', `出错了: ${errMsg}`);
        } finally {
            sendBtn.disabled = false;
            userInput.focus();
        }
    }

    async function newSession() {
        try {
            const res = await axios.post('/api/new-session');
            sessionId = res.data.session_id;
            container.innerHTML = '';
            addMessage('ai', '新对话已开始,有什么可以帮你的吗?');
        } catch (err) {
            alert('创建新对话失败');
        }
    }

    // 页面加载时创建会话
    newSession();
</script>

</body>
</html>

五、运行效果

启动服务:

python server.py

浏览器打开 http://127.0.0.1:5000,即可看到聊天界面:

- 紫色渐变气泡为用户消息,白色气泡为 AI 回复

- AI 回复支持 Markdown 格式(代码块、列表等)

- 等待回复时有跳动的加载动画

- 点击「新对话」清空历史、重新开始

- 支持 Enter 发送、Shift+Enter 换行

六、项目结构

test-project/

├── .env                  # API Key 配置(不入库)

├── chat_qwen.py          # 命令行版聊天机器人

├── server.py             # Flask Web 后端

└── static/

    └── index.html        # 前端聊天界面

七、后续扩展方向

这个项目是一个最小可用的基础版本,在此基础上可以扩展很多能力:

| 方向 | 说明 |

| 流式输出 | 用 SSE(Server-Sent Events)实现逐字输出,体验更流畅 |

| RAG 知识库 | 接入向量数据库,让 AI 基于你的文档回答问题 |

| Agent 工具调用 | 给 AI 加上搜索、查天气、执行代码等工具能力 |

| 用户系统 | 加登录注册,对话历史存数据库(SQLite/MySQL) |

| 部署上线 | 用 Gunicorn + Nginx 部署到云服务器 |


注意:仅供参考!!!

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐