场景落地篇01-我把橘子洲头做成了AI客服:本地大模型落地的第一个真实场景
老攻城狮一枚,励志要好好学习天天向上,分享20多年实战项目中的点点滴滴,顺带引导家里小公举学AI编程。 编程点滴,留下足迹; 分享交流,共勉加油。也许朋友们会问:为什么一个20+年IT老兵决定系统性学习AI人工智能

今天实践分享的内容很接地气,这个场景是非常典型的,最容易实施的。大家可以跟着一步一步来,和我一起动手搭建一个【橘子洲头景区AI客服】的完整AI应用场景。这个版本是延续之前跑通的Flask + Ollama方案:老攻城狮的AI开发环境搭建全记录:从零到跑通本地大模型(一日速通版),用核心景区“长沙橘子洲头”的案例做了个性化的场景化改造——加入景区知识库、定制Prompt、优化UI体验。
话不多说,直接上干货,接下来我的每一步操作都配有完整代码和操作说明,亲爱的读者!您也可以和我一起码动起来(环境搭建等相关的基础内容请参考之前的文章,这里就不啰嗦了)
一、项目结构
这次我们用一个新目录 orange_island_ai,与之前的知识库案例分开,保持工程清晰,目录结构如下:
~/ai_project/orange_island_ai/ ├── app.py # Flask 主程序 ├── templates/ │ └── index.html # 前端页面 ├── knowledge_base/ │ └── qa_pairs.json # 橘子洲头问答库(30条) ├── static/ │ └── style.css # (可选)自定义样式 └── requirements.txt # 依赖清单
二、环境准备
2.1 进入项目目录
cd ~/ai_project
mkdir orange_island_ai && cd orange_island_ai
mkdir templates knowledge_base static
2.2 激活虚拟环境并安装依赖
source ../venv/bin/activate
pip install flask requests
2.3 确认Ollama服务运行
确保Ollama在后台运行,且qwen2.5:7b模型已下载。
ollama list
# 应显示 qwen2.5:7b
如果未下载,执行:
ollama run qwen2.5:7b
# 下载完成后输入 /bye 退出
三、知识库数据:knowledge_base/qa_pairs.json
将以下30条问答对保存为 knowledge_base/qa_pairs.json:
{
"景区信息": [
{
"q": "橘子洲头景区的开放时间是什么?",
"a": "橘子洲头景区全年开放,开放时间为07:00-22:00,21:00停止入园。建议游客在21:00前完成入园,景区闭园前离开即可。"
},
{
"q": "橘子洲头需要门票吗?门票多少钱?",
"a": "橘子洲头景区门票免费,但所有游客必须提前实名预约入园。景区无大门票费用,免费开放。"
},
{
"q": "观光小火车的票价是多少?有优惠吗?",
"a": "观光小火车环全洲线往返票40元/人。以下人群享受半价优惠(20元/人):18周岁以下未成年人、60周岁以上老年人、全日制大学本科及以下在校学生、残疾人、现役军人等。1.2米以下儿童免票。"
},
{
"q": "观光小火车的运营时间是什么?",
"a": "观光小火车运营时间分冬夏季:夏季(5月1日-10月31日)7:00-22:00,冬季(11月1日-4月30日)7:30-21:30。建议留意末班车时间,避免滞留。"
},
{
"q": "湘江游轮的价格是多少?",
"a": "湘江游轮日游票约120元/人起,夜游票约188元/人起。白天观光游轮航程约40分钟至1小时,夜景游轮航程约一个半小时,可欣赏湘江两岸灯光秀。"
}
],
"交通与停车": [
{
"q": "怎么去橘子洲头最方便?",
"a": "地铁2号线是最佳选择,乘坐地铁2号线至橘子洲·青莲站,从1号口或2号口出站即达景区入口。出站后左手边就是观光小火车始发站,步行不到50米。"
},
{
"q": "可以自驾去橘子洲头吗?",
"a": "周末及节假日社会车辆禁行上岛。工作日自驾需提前在岳麓山橘子洲旅游区微信公众号预约车辆入园。景区周边停车场车位紧张且易堵车,强烈建议乘坐地铁。"
},
{
"q": "橘子洲头有停车场吗?收费标准是什么?",
"a": "景区有东停车场和西停车场两个停车场。收费标准为:首小时10元,后续每小时5元,单日最高50元。东停车场靠近毛主席雕像,位置较紧俏。"
},
{
"q": "如果景区停车场满了怎么办?",
"a": "可停到周边步步高广场、五一广场或溁湾镇地铁站附近的商业停车场,再坐地铁前往景区。停好车后坐地铁2号线到橘子洲站,仅需一两站。"
},
{
"q": "可以坐公交车去橘子洲头吗?",
"a": "可以。109路、152路、913路等公交车可到达橘子洲景区附近。但公交车下车点在景区外,需要步行过桥,不如地铁方便。地铁2号线直达景区内,是最优选择。"
}
],
"预约与入园": [
{
"q": "去橘子洲头需要预约吗?",
"a": "必须预约。橘子洲头实行实名预约制度,所有游客(含老人、儿童)均需提前预约,无免预约绿色通道。未预约无法入园。"
},
{
"q": "怎么预约橘子洲头门票?",
"a": "通过官方唯一渠道岳麓山橘子洲旅游区微信公众号预约。也可通过趣长沙小程序预约。预约成功后凭二维码或身份证原件刷证入园。"
},
{
"q": "可以提前多少天预约?",
"a": "可提前3天预约,每日0点更新门票。节假日建议放票即抢,高峰时段当日票基本秒空。"
},
{
"q": "预约分时段吗?每个时段有多少名额?",
"a": "分三个时段预约:早上07:00-12:00:4万人;中午12:00-17:00:3万人;晚上17:00-22:00:1万人。需在所选时间段内入园,超时需重新预约。"
},
{
"q": "入园需要带什么证件?",
"a": "携带预约时登记的身份证原件,刷证或刷脸通行。一人一证,不可转借。安检禁止携带易燃易爆品、三脚架、自拍杆及大件行李。"
}
],
"景点与游玩": [
{
"q": "橘子洲头有哪些必打卡景点?",
"a": "核心景点包括:青年毛泽东艺术雕塑(高32米,世界最大毛泽东雕像)、问天台(沁园春·长沙出处)、沁园春·长沙诗词碑、望江亭(唐代古风,270°江景)、百亩橘园、谁主沉浮雕塑群、长沙市非物质文化遗产展示馆、朱张古渡、唐生智公馆。"
},
{
"q": "橘子洲头推荐游览时间多长?",
"a": "推荐游览时间3-5小时。橘子洲全长约5公里,纯步行往返超10公里。90%的游客选择乘坐观光车。"
},
{
"q": "毛泽东青年艺术雕塑有多大?",
"a": "雕塑高32米,长83米,宽41米,是目前世界上最大的毛泽东雕像。由8000多块花岗岩拼接而成,以1925年青年时期的毛泽东形象为原型。"
},
{
"q": "橘子洲头有烟花表演吗?",
"a": "特定节日有烟花燃放活动。烟花燃放时间通常为20:30-20:50。烟花燃放当天下午16:00禁止入园,具体以景区实际公示为准。杜甫江阁是最佳观赏点之一。"
},
{
"q": "橘子洲头有什么特色体验?",
"a": "特色体验包括:瞻仰32米伟人雕塑、乘坐观光小火车吹江风看长沙城景、文创店免费集章(最多138枚)、夜游欣赏19:30江岸灯光秀、秋季可在橘洲文化园体验摘橘子。"
}
],
"观光车与游船": [
{
"q": "观光小火车值得坐吗?",
"a": "非常值得。橘子洲全长约5公里,步行往返超10公里。观光车全程往返40元/人,可在各站点随上随下。90%的游客选择乘坐观光车。务必购买往返票,单程走断腿。"
},
{
"q": "观光小火车怎么购票?",
"a": "通过长沙城发橘子洲微信小程序线上购票,或现场自助售票机、人工售票窗口购票。购票后凭电子检票码扫码乘车,无需兑换纸质票。"
},
{
"q": "观光小火车的站点有哪些?",
"a": "主要站点包括:橘子洲站(地铁入口始发站)、神职人员寓所站、新诗词碑站、问天台站(青年毛泽东雕像)、橘洲客栈站、沙滩公园站、江神庙站。车票当日有效,同一站点只可乘车一次。"
},
{
"q": "观光小火车的优待票怎么买?",
"a": "18周岁以下未成年人、60周岁以上老年人可凭身份证在自助机上购买优待票。其他优待票(在校学生、退役军人等)需在人工售票窗口购买。"
},
{
"q": "水陆两栖车是什么?值得体验吗?",
"a": "水陆两栖车是橘子洲近年推出的网红交通工具,既能在陆地上跑,又能直接开到水里。运营时间需以景区当日公告为准(夏季通常17:00停止营运)。建议提前确认运营时间,避免白跑一趟。"
}
],
"错峰与贴士": [
{
"q": "什么时间去橘子洲头人最少?",
"a": "推荐两个黄金时段:清晨7:30-9:00(游客稀少、光线柔和,适合拍照)和傍晚17:00-22:00(人流最少、可欣赏日落+夜景)。中午12:00-14:00是全天客流低谷,但阳光强烈、遮阴少。"
},
{
"q": "橘子洲头游玩有什么注意事项?",
"a": "必须提前预约,未预约无法入园;穿舒适的鞋,景区超长,暴走会崩溃;不要相信黄牛,官方预约免费;携带身份证原件入园;禁止携带三脚架、自拍杆及大件行李。"
},
{
"q": "橘子洲头附近有什么好吃的?",
"a": "长沙特色美食包括:黑色经典臭豆腐、糖油粑粑、剁椒鱼头、辣椒炒肉、口味虾、嗦螺、酱板鸭、湘莲。景区内有江天暮雪风物市集,涵盖31家商铺。"
},
{
"q": "橘子洲头适合什么季节去?",
"a": "橘子洲四时之景各异:春季(2月中旬至3月樱花盛开,梅园梅花绽放)、秋季(橘黄橘绿之秋景最佳,可体验摘橘子)。全年四季皆宜,各有特色。"
},
{
"q": "带老人小孩去橘子洲头有什么建议?",
"a": "务必乘坐观光小火车(老人、儿童享半价优惠);提前3天预约,选早上或傍晚时段避开人流高峰;1.2米以下儿童观光车免票;景区内各站点均可上下车,不必一次性走完全程;注意末班车时间,避免滞留。"
}
]
}
四、后端代码:app.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
橘子洲头景区AI客服 - Flask后端
功能:
1. 提供Web聊天界面
2. 接收用户问题,调用Ollama API生成回答
3. 支持基于本地知识库的精准问答
4. 包含景区客服专属Prompt
作者:Javy21 (https://blog.csdn.net/javy21)
环境:WSL2 Ubuntu 22.04 / Python 3.10 / Ollama + Qwen2.5:7B
"""
import os
import json
import requests
from flask import Flask, render_template, request, jsonify
# ---------- 配置 ----------
app = Flask(__name__)
# Ollama服务地址(根据实际环境修改)
OLLAMA_URL = "http://192.168.0.106:11434/api/generate"
MODEL_NAME = "qwen2.5:7b"
# 加载本地知识库
KNOWLEDGE_BASE = []
try:
with open('./knowledge_base/qa_pairs.json', 'r', encoding='utf-8') as f:
data = json.load(f)
for category, pairs in data.items():
for pair in pairs:
KNOWLEDGE_BASE.append({
'question': pair['q'],
'answer': pair['a'],
'category': category
})
print(f"✅ 加载 {len(KNOWLEDGE_BASE)} 条知识库问答")
except Exception as e:
print(f"⚠️ 知识库加载失败: {e}")
KNOWLEDGE_BASE = []
# ---------- 系统Prompt ----------
SYSTEM_PROMPT = """你是橘子洲头景区的AI客服助手,名字叫"橘子小助手"。
【你的角色】
你是橘子洲头景区的官方智能客服,为游客提供准确、友好、高效的咨询服务。
【回答规则】
1. 优先使用知识库中的标准答案回答
2. 如果知识库中没有,基于你的通用知识进行合理回答
3. 回答必须与橘子洲头景区相关,不回答无关问题
4. 语气热情友好,带长沙特色(可以偶尔用"霸得蛮"、"韵味"等长沙方言点缀)
【景区基本信息】
- 名称:橘子洲头景区
- 开放时间:07:00-22:00
- 门票:免费(需预约)
- 地址:湖南省长沙市岳麓区橘子洲头2号
- 地铁:2号线橘子洲·青莲站
【核心服务】
- 预约入园:微信公众号"岳麓山橘子洲旅游区"
- 观光车:环全洲线往返40元/人
- 游船:日游120元起,夜游188元起
【回答要求】
- 简洁明了,关键信息加粗
- 涉及价格、时间等数字信息必须准确
- 不确定的信息要明确告知"建议以景区官方公告为准"
- 回答控制在200字以内
"""
# ---------- 本地检索函数 ----------
def search_knowledge(query):
"""
在本地知识库中检索匹配的问答对
简单关键词匹配,返回最相关的一条
"""
query_lower = query.lower()
best_match = None
best_score = 0
for item in KNOWLEDGE_BASE:
q = item['question'].lower()
# 简单的关键词匹配评分
score = 0
keywords = query_lower.split()
for kw in keywords:
if kw in q:
score += 1
# 完整匹配加分
if query_lower in q:
score += 5
if score > best_score:
best_score = score
best_match = item
# 只有匹配度 > 0 才返回,否则返回None
if best_score > 0:
return best_match
return None
# ---------- 路由 ----------
@app.route('/')
def index():
"""渲染主页面"""
return render_template('index.html')
@app.route('/api/chat', methods=['POST'])
def chat():
"""
聊天API
请求体:{"message": "用户问题"}
返回:{"reply": "AI回答"}
"""
data = request.get_json()
user_message = data.get('message', '').strip()
if not user_message:
return jsonify({'error': '请输入您的问题'}), 400
# 1. 先查本地知识库
matched = search_knowledge(user_message)
if matched:
# 直接使用知识库答案
reply = f"{matched['answer']}\n\n(以上信息来自橘子洲头景区官方指南)"
print(f"📚 命中知识库: {matched['question'][:30]}...")
else:
# 2. 调用大模型
print(f"🤖 调用大模型: {user_message[:30]}...")
# 构造Prompt
prompt = f"""{SYSTEM_PROMPT}
游客提问:{user_message}
请以橘子洲头景区AI客服的身份,回答游客的问题。如果不知道答案,请礼貌告知并建议查看官方渠道。"""
payload = {
'model': MODEL_NAME,
'prompt': prompt,
'stream': False,
'options': {
'temperature': 0.2,
'num_predict': 512
}
}
try:
response = requests.post(OLLAMA_URL, json=payload, timeout=60)
response.raise_for_status()
result = response.json()
reply = result.get('response', '抱歉,我暂时无法回答这个问题。')
except requests.exceptions.Timeout:
reply = '抱歉,AI思考时间过长,请稍后再试。'
except Exception as e:
print(f"❌ Ollama调用失败: {e}")
reply = '抱歉,AI服务暂时不可用,请稍后再试。'
return jsonify({'reply': reply})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5003, debug=True)
五、前端页面:templates/index.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>
<style>
/* ---------- 全局重置 ---------- */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: #f0f4f8;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
/* ---------- 主容器 ---------- */
.chat-container {
width: 100%;
max-width: 420px;
height: 92vh;
max-height: 780px;
background: #ffffff;
border-radius: 24px;
box-shadow: 0 20px 60px rgba(0,0,0,0.15);
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
/* ---------- 头部 ---------- */
.header {
background: linear-gradient(135deg, #2d7d46 0%, #1a5c33 100%);
padding: 20px 24px 16px;
color: #fff;
flex-shrink: 0;
}
.header-top {
display: flex;
align-items: center;
gap: 12px;
}
.header .avatar {
width: 44px;
height: 44px;
background: rgba(255,255,255,0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
flex-shrink: 0;
}
.header .title {
font-size: 18px;
font-weight: 700;
}
.header .subtitle {
font-size: 12px;
opacity: 0.85;
margin-top: 2px;
}
.header .status {
font-size: 11px;
opacity: 0.8;
margin-top: 6px;
display: flex;
align-items: center;
gap: 6px;
}
.header .status .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #7bed9f;
display: inline-block;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
/* ---------- 快捷入口 ---------- */
.quick-actions {
padding: 12px 16px 8px;
display: flex;
gap: 8px;
flex-wrap: wrap;
flex-shrink: 0;
border-bottom: 1px solid #f0f0f0;
}
.quick-actions .tag {
padding: 6px 14px;
background: #f0f7f3;
color: #2d7d46;
border-radius: 20px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
white-space: nowrap;
}
.quick-actions .tag:hover {
background: #2d7d46;
color: #fff;
}
/* ---------- 消息区域 ---------- */
.messages {
flex: 1;
padding: 16px 20px;
overflow-y: auto;
background: #fafcfe;
}
.messages::-webkit-scrollbar {
width: 4px;
}
.messages::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 4px;
}
.message {
margin-bottom: 16px;
display: flex;
flex-direction: column;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.message .bubble {
max-width: 88%;
padding: 12px 16px;
border-radius: 16px;
font-size: 15px;
line-height: 1.7;
word-wrap: break-word;
}
.message.user {
align-items: flex-end;
}
.message.user .bubble {
background: #2d7d46;
color: #fff;
border-bottom-right-radius: 4px;
}
.message.assistant {
align-items: flex-start;
}
.message.assistant .bubble {
background: #f0f0f0;
color: #1a2332;
border-bottom-left-radius: 4px;
}
.message .time {
font-size: 11px;
color: #999;
margin-top: 4px;
padding: 0 4px;
}
/* ---------- 欢迎消息 ---------- */
.welcome {
text-align: center;
padding: 20px 0 8px;
}
.welcome .icon { font-size: 48px; }
.welcome h3 {
font-size: 18px;
color: #1a2332;
margin: 8px 0 4px;
}
.welcome p {
font-size: 14px;
color: #6b7a8f;
}
/* ---------- 输入区域 ---------- */
.input-area {
display: flex;
padding: 12px 16px 16px;
border-top: 1px solid #eee;
background: #fff;
flex-shrink: 0;
gap: 10px;
}
.input-area input {
flex: 1;
padding: 12px 16px;
border: 1.5px solid #e0e6ed;
border-radius: 24px;
outline: none;
font-size: 15px;
transition: border-color 0.2s;
}
.input-area input:focus {
border-color: #2d7d46;
}
.input-area input:disabled {
background: #f5f7fa;
}
.input-area button {
padding: 12px 24px;
background: #2d7d46;
color: #fff;
border: none;
border-radius: 24px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
flex-shrink: 0;
}
.input-area button:hover {
background: #1f5c33;
}
.input-area button:disabled {
background: #aab3c5;
cursor: not-allowed;
}
/* ---------- 底部安全区 ---------- */
.footer {
text-align: center;
font-size: 11px;
color: #bbb;
padding: 6px 0 10px;
flex-shrink: 0;
border-top: 1px solid #f5f5f5;
}
.footer a {
color: #2d7d46;
text-decoration: none;
}
/* ---------- 响应式 ---------- */
@media (max-width: 460px) {
.chat-container {
max-width: 100%;
height: 100vh;
max-height: 100vh;
border-radius: 0;
box-shadow: none;
}
body { padding: 0; }
}
</style>
</head>
<body>
<div class="chat-container">
<!-- 头部 -->
<div class="header">
<div class="header-top">
<div class="avatar">🍊</div>
<div>
<div class="title">橘子洲头·智能客服</div>
<div class="subtitle">橘子小助手 · 7×24小时在线</div>
</div>
</div>
<div class="status">
<span class="dot"></span>
<span>已接入「橘子洲头景区」知识库</span>
</div>
</div>
<!-- 快捷入口 -->
<div class="quick-actions" id="quickTags">
<span class="tag" data-msg="橘子洲头开放时间是什么?">🕐 开放时间</span>
<span class="tag" data-msg="观光小火车票价多少?">🚂 小火车票价</span>
<span class="tag" data-msg="怎么去橘子洲头?">🚇 交通路线</span>
<span class="tag" data-msg="需要预约吗?怎么预约?">📱 预约入园</span>
<span class="tag" data-msg="毛泽东雕塑有多大?">🗿 伟人雕塑</span>
<span class="tag" data-msg="橘子洲头有什么好吃的?">🍜 美食推荐</span>
</div>
<!-- 消息区域 -->
<div class="messages" id="messages">
<div class="welcome" id="welcome">
<div class="icon">🍊</div>
<h3>欢迎来到橘子洲头!</h3>
<p>我是橘子小助手,可以帮你查询<br>开放时间、门票、交通、景点等一切信息~</p>
</div>
</div>
<!-- 输入区域 -->
<div class="input-area">
<input type="text" id="userInput" placeholder="输入你的问题..." />
<button id="sendBtn">发送</button>
</div>
<div class="footer">
<a href="https://blog.csdn.net/javy21" target="_blank">老攻城狮的AI编程实践之路</a> · 橘子洲头景区AI客服
</div>
</div>
<script>
// ============================================================
// 橘子洲头AI客服 - 前端交互
// 作者:Javy21 (https://blog.csdn.net/javy21)
// ============================================================
const messages = document.getElementById('messages');
const input = document.getElementById('userInput');
const sendBtn = document.getElementById('sendBtn');
const welcome = document.getElementById('welcome');
/**
* 添加一条消息
*/
function addMessage(role, content) {
// 隐藏欢迎语
if (welcome) welcome.style.display = 'none';
const div = document.createElement('div');
div.className = `message ${role}`;
const bubble = document.createElement('div');
bubble.className = 'bubble';
bubble.innerHTML = content.replace(/\n/g, '<br>');
div.appendChild(bubble);
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
}
/**
* 设置加载状态
*/
function setLoading(loading) {
sendBtn.disabled = loading;
input.disabled = loading;
sendBtn.textContent = loading ? '思考中...' : '发送';
if (loading) {
const loadingDiv = document.createElement('div');
loadingDiv.className = 'message assistant';
loadingDiv.id = 'loadingMsg';
loadingDiv.innerHTML = `<div class="bubble" style="background:#f0f0f0;color:#888;">⏳ 正在思考...</div>`;
messages.appendChild(loadingDiv);
messages.scrollTop = messages.scrollHeight;
} else {
const loadingMsg = document.getElementById('loadingMsg');
if (loadingMsg) loadingMsg.remove();
}
}
/**
* 发送消息
*/
async function sendMessage() {
const text = input.value.trim();
if (!text) return;
// 显示用户消息
addMessage('user', text);
input.value = '';
setLoading(true);
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: text })
});
const data = await response.json();
if (response.ok) {
addMessage('assistant', data.reply);
} else {
addMessage('assistant', `❌ 错误:${data.error || '服务异常'}`);
}
} catch (err) {
addMessage('assistant', `❌ 网络错误:${err.message}`);
} finally {
setLoading(false);
input.focus();
}
}
// ---------- 事件绑定 ----------
sendBtn.addEventListener('click', sendMessage);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') sendMessage();
});
// 快捷标签点击
document.querySelectorAll('.tag').forEach(tag => {
tag.addEventListener('click', function() {
input.value = this.dataset.msg;
sendMessage();
});
});
// 聚焦
input.focus();
</script>
</body>
</html>
六、运行与验证
6.1 启动服务
cd ~/ai_project/orange_island_ai
source ../venv/bin/activate
python app.py
看到以下输出表示成功:
✅ 加载 30 条知识库问答
* Running on http://0.0.0.0:5003
6.2 访问测试
浏览器打开 http://localhost:5003
测试问题:
-
“橘子洲头开放时间是什么?” → 应命中知识库,返回标准答案
-
“观光小火车票价多少?” → 应命中知识库
-
“橘子洲头有什么好吃的?” → 应命中知识库
-
“长沙还有什么好玩的?” → 知识库无匹配,调用大模型
6.3 验证结果
| 测试场景 | 预期结果 |
|---|---|
| 知识库命中 | 秒回标准答案,带“来自橘子洲头景区官方指南” |
| 知识库未命中 | 调用大模型,以客服身份回答 |
| 快捷标签点击 | 自动填充并发送 |
【写在最后】橘子洲头的AI客服应用参考示例,从构思、设计、编码、部署、测试一步步落地实施,我已经完整跑通了,并记录了下来整个过程。代码全部开源,考虑版权和数据安全问题,项目的数据全部来自公开信息(如有请涉嫌版权,请联系博主进行删除),您拿到就能用、用了就能跑。
接下来我想听听您的声音:
-
您所在的城市,有没有适合做成AI客服的景区或公共场景?
-
您觉得以上的这个实例里面,最值得扩展的功能是什么?
-
如果把这套方案落地到真实生产环境,您最担心的技术或运维问题是什么?
欢迎在评论区留言,我会认真对待每一位读者,认真看每一条回复,挑有代表性的问题进行专题解答。
如果这篇内容对您有帮助,麻烦您动动发财的手,帮忙点个“赞”和“收藏”,让更多正在探索AI落地的同行看到。有任何疑问,或者想看其他场景的AI应用实践,也欢迎告诉我——下一篇文章的主题,可能就来自您的建议。灰常感谢。
更多推荐


所有评论(0)