我把橘子洲头做成了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 static2.2 激活虚拟环境并安装依赖source ../venv/bin/activate pip install flask requests2.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:0021: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:004万人中午12:00-17:003万人晚上17:00-22:001万人。需在所选时间段内入园超时需重新预约。 }, { 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, encodingutf-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, jsonpayload, timeout60) 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(host0.0.0.0, port5003, debugTrue)五、前端页面templates/index.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.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 classchat-container !-- 头部 -- div classheader div classheader-top div classavatar/div div div classtitle橘子洲头·智能客服/div div classsubtitle橘子小助手 · 7×24小时在线/div /div /div div classstatus span classdot/span span已接入「橘子洲头景区」知识库/span /div /div !-- 快捷入口 -- div classquick-actions idquickTags span classtag>cd ~/ai_project/orange_island_ai source ../venv/bin/activate python app.py看到以下输出表示成功✅ 加载 30 条知识库问答 * Running on http://0.0.0.0:50036.2 访问测试浏览器打开http://localhost:5003测试问题“橘子洲头开放时间是什么” → 应命中知识库返回标准答案“观光小火车票价多少” → 应命中知识库“橘子洲头有什么好吃的” → 应命中知识库“长沙还有什么好玩的” → 知识库无匹配调用大模型6.3 验证结果测试场景预期结果知识库命中秒回标准答案带“来自橘子洲头景区官方指南”知识库未命中调用大模型以客服身份回答快捷标签点击自动填充并发送【写在最后】橘子洲头的AI客服应用参考示例从构思、设计、编码、部署、测试一步步落地实施我已经完整跑通了并记录了下来整个过程。代码全部开源考虑版权和数据安全问题项目的数据全部来自公开信息如有请涉嫌版权请联系博主进行删除您拿到就能用、用了就能跑。接下来我想听听您的声音您所在的城市有没有适合做成AI客服的景区或公共场景您觉得以上的这个实例里面最值得扩展的功能是什么如果把这套方案落地到真实生产环境您最担心的技术或运维问题是什么欢迎在评论区留言我会认真对待每一位读者认真看每一条回复挑有代表性的问题进行专题解答。如果这篇内容对您有帮助麻烦您动动发财的手帮忙点个“赞”和“收藏”让更多正在探索AI落地的同行看到。有任何疑问或者想看其他场景的AI应用实践也欢迎告诉我——下一篇文章的主题可能就来自您的建议。灰常感谢。