最近不少朋友在问:“想做一款能自动回答用户问题的微信小程序,后端用 Python 接 AI,到底怎么落地?”正好我这边刚把一个基于 AI 的在线答疑系统从零到一跑通,包含小程序前端、Python 后端、知识库检索和内容安全过滤,今天把完整的拆解思路、核心代码和踩坑记录整理出来。这套方案适合三类人:一是想给自己的课程或产品做智能问答助手的开发者,二是正在准备毕设或实训项目的学生,三是想了解“小程序 + 大模型 API”怎么结合的产品经理。我会尽量把每一步的“为什么这么做”也讲清楚,而不是只贴代码。
1. 项目整体设计与架构拆解
1.1 这个答疑系统到底解决了什么问题
传统的在线答疑通常靠人工客服或者论坛帖子的形式,回答时效性差、重复问题多、人力成本高。答疑系统的核心目标,是把用户经常问的高频问题交给 AI 自动回答,人工只处理 AI 解决不了的部分。举个例子,某高校的课程答疑平台每天收到几百条类似“作业截止日期”“考试范围”“实验报告格式”的问题,这些问题答案固定、重复度高,非常适合做成知识库让 AI 检索回答。
基于这一需求,我把系统拆成三个端:用户通过微信小程序提问并查看答案,Python 后端负责处理请求、调用 AI 接口、管理会话历史,管理端(可以是简单的 Web 页面)负责维护知识库文档。这种三层结构的好处是每层职责单一,后续替换 AI 服务商或者更换前端框架都不会互相影响。实际项目里,我把小程序端仅作为展示层,所有业务逻辑都收敛在后端,这样也方便未来扩展成 App 或 Web 端。
1.2 技术选型背后的取舍
技术栈我最终选的是:微信小程序原生框架 + Python(FastAPI)+ 大模型 API + 轻量级向量检索。这套组合不是随便定的,每个选择都有具体考量。
先说小程序,为什么不选 uni-app 或 Taro。我的目标是做一个功能中等、需要快速上线的答疑工具,原生框架的调试工具最成熟、API 文档最全,遇到问题搜到的解决方案最多。如果你要一套代码同时跑微信和支付宝小程序,那 uni-app 更合适,但我建议第一版别给自己加这种负担,先把一个平台跑通。
后端选 FastAPI 而不是 Flask,主要看中三点:异步支持好,调用 AI 接口属于 IO 密集型操作,异步能显著提高并发处理能力;自带 Pydantic 参数校验,避免手写大量的请求体判断;自动生成 OpenAPI 文档,调试接口非常方便。如果你的部署环境对版本要求苛刻,或者团队只熟悉 Flask,那 Flask 也能做,但异步这块需要额外引入 asyncio 库,稍微麻烦一些。
AI 能力直接调用大模型开放平台的 API,而不是自己训练模型。自己训练一个大模型需要的算力和数据量,个人开发者基本扛不住,而且答疑场景下模型的准确性远不如“知识库 + 大模型”的组合。大模型只负责理解问题和组织语言,具体的领域知识从知识库检索获得,这样既保证了回答的时效性,又能控制成本。
1.3 系统架构图与数据流向
整个系统的请求链路是这样的:用户在微信小程序输入问题,小程序通过 wx.request 将问题 POST 到后端 /api/ask 接口,后端拿到问题后先做内容安全检测,再判断是走知识库检索还是直接交给大模型,最后把回答结果返回给小程序渲染。用户的历史记录会存入 MySQL 数据库,方便随时查看。
知识库的构建路径则是:管理员把 PDF、Word、TXT 等文档上传到管理端,后端对文本进行清洗和切片,通过 Embedding 模型转成向量存入向量数据库。每次用户提问时,后端把问题转成向量,在库里检索最相关的几个文本片段,拼接到系统提示词中交给大模型,模型再基于这些材料生成最终回答。
数据库选型上,MySQL 存储用户信息、提问记录和反馈,向量数据库负责知识库检索。前期数据量不大时,用轻量级的向量方案完全够,没必要一上来就上分布式搜索引擎,这是很多新手容易犯的过度设计毛病。
2. 小程序端核心模块:从提问入口到答案渲染
2.1 页面结构设计与跳转流程
小程序的页面我控制在五个以内:首页(提问框 + 推荐问题)、回答页(流式展示答案)、历史记录页、我的页面、管理后台入口(仅管理员可见)。页面少的好处是审核容易通过,而且用户学习成本低。
首页的提问框是整个产品的心脏,需要注意几个细节:输入框要有防抖处理,用户在 300 毫秒内快速多次点击提交按钮时只发送一次请求;提交后按钮进入 loading 状态,禁止重复提交;输入内容为空时给出提示,前后端都要校验。推荐问题列表我用的是硬编码数据,这些数据来自后台统计的高频问题,每周更新一次即可,没必要做成动态接口。
回答页我采用“先显示问题,再逐步渲染答案”的方式,避免用户等待时面对空白屏。小程序端没有原生的流式渲染能力,我实现了一个相对简单但效果不错的方案:后端一次性返回完整回答,前端通过 JS 定时器配合字符串截取,模拟打字机效果。这个方案虽然不如真正的 Server-Sent Events 流式返回流畅,但胜在实现简单、稳定,用户体验也不差。
2.2 请求封装与本地缓存策略
小程序的网络请求不能裸用 wx.request,我封装了一个 request.js 模块,统一处理 BASE_URL 配置、请求超时、状态码判断、错误提示。核心代码长这样:
// utils/request.js const BASE_URL = 'https://api.example.com' // 换成你自己的域名 function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: `${BASE_URL}${path}`, method: method, data: data, header: { 'Content-Type': 'application/json' }, timeout: 30000, success: (res) => { if (res.statusCode === 200) { resolve(res.data) } else { wx.showToast({ title: '服务异常,请稍后重试', icon: 'none' }) reject(res) } }, fail: (err) => { wx.showToast({ title: '网络连接失败', icon: 'none' }) reject(err) } }) }) } module.exports = { request }这里用 Promise 包装是因为 async/await 在小程序基础库里的支持情况比较微妙,Promise 方案最保险。超时时间我设了 30 秒,因为 AI 接口响应通常在 2 到 10 秒之间,太短会频繁超时,太长用户会等得不耐烦。
历史记录用 wx.setStorageSync 存在本地,数据结构是 JSON 数组,每条包含 question、answer、timestamp 三个字段。这里有个坑:本地存储容量限制是 10MB,如果用户提问特别多,数组会撑爆。我的解决方案是只保留最近 50 条记录,超出部分自动丢弃,并在用户进入历史页时通过分页接口拉取更多。
2.3 录音提问和图片识别的扩展预留
答疑场景里,用户可能在移动端不方便打字,所以我在第一版就预留了语音入口。小程序端通过 wx.getRecorderManager() 录音,然后把音频文件上传到后端,后端调用语音转文字服务变成文本,再走正常的提问链路。
图片识别稍微复杂一些,涉及拍照上传、OCR 文字识别、再进入知识库检索。不少学生问的是“这道题怎么做”,如果能把题目拍照上传,识别文字后匹配相似题,体验会提升一个档次。不过这两块功能工作量不小,我建议你第一版先不做,等文本问答稳定运营后再迭代,但前端页面可以把入口先埋好,避免后期改版。
3. Python 后端核心实现:API 设计、数据库与安全
3.1 FastAPI 项目结构搭建
后端项目我采用分模块结构,不做大杂烩单文件:
project/ ├── app/ │ ├── main.py # FastAPI 入口 │ ├── config.py # 配置管理 │ ├── models/ # 数据库模型 │ ├── routers/ # 路由文件 │ ├── services/ # 业务逻辑层 │ └── utils/ # 通用工具 ├── requirements.txt └── .env # 环境变量为什么要分模块?因为答疑系统后续一定会扩展功能,比如新增用户反馈接口、管理端接口,如果所有路由都写在 main.py 里,几百行代码挤在一起,排查问题会非常痛苦。分模块之后,每个业务域一个路由文件,新人接手也能快速定位。
main.py 的主逻辑很简单,创建 FastAPI 实例、加载 CORS 配置、注册路由、启动时初始化数据库:
# app/main.py from fastapi import FastAPI from app.routers import ask, history, feedback, knowledge from app.utils.database import init_db app = FastAPI(title="AI 答疑系统 API", version="1.0.0") app.include_router(ask.router, prefix="/api") app.include_router(history.router, prefix="/api") app.include_router(feedback.router, prefix="/api") app.include_router(knowledge.router, prefix="/api") @app.on_event("startup") async def startup_event(): init_db()需要注意 CORS 配置这次其实用不上,因为小程序请求后端不受浏览器同源策略限制,但我还是加上了,方便以后要用 Web 管理端调试。运行时用 uvicorn 启动,指定 host 为 0.0.0.0 才能让局域网访问到。
3.2 /api/ask 接口的完整实现逻辑
这是全系统最核心的接口,我把它做成 POST 请求,接收 JSON 参数:question 用户问题、session_id 会话标识、user_id 可选。处理流程分成四步:参数校验、安全检测、生成回答、保存历史。完整代码片段如下:
# app/routers/ask.py from fastapi import APIRouter, HTTPException from pydantic import BaseModel from app.services.ai_service import generate_answer from app.services.security import check_content_safety from app.models.history import save_history from app.utils.cache import get_cache, set_cache router = APIRouter() class AskRequest(BaseModel): question: str session_id: str = "" user_id: str = "" @router.post("/ask") async def ask(req: AskRequest): # 1. 基础校验 if not req.question or len(req.question.strip()) < 2: raise HTTPException(status_code=400, detail="问题内容太短") if len(req.question) > 500: raise HTTPException(status_code=400, detail="问题长度超出限制") # 2. 内容安全检测 if not check_content_safety(req.question): raise HTTPException(status_code=400, detail="内容包含不合适的信息") # 3. 缓存查找(完全相同的题目直接返回) cache_key = f"qa:{req.question}" cached = get_cache(cache_key) if cached: return {"answer": cached, "source": "cache"} # 4. 调用 AI 服务 answer = await generate_answer(req.question) # 5. 写缓存和历史记录 set_cache(cache_key, answer, ttl=3600) save_history(req.user_id, req.session_id, req.question, answer) return {"answer": answer, "source": "ai"}缓存这一层很多人会忽略,但它对降低成本和提升响应速度立竿见影。答疑场景中重复问题比例相当高,同一个问题在一天内被问几十次很常见,加了 Redis 缓存后,这些问题不再消耗大模型 API 的调用次数,每个缓存命中基本是毫秒级返回。
3.3 用户身份识别与 openid 获取
小程序没有传统的用户名密码体系,用户身份靠微信的 openid 来区分。小程序端通过 wx.login 获取临时 code,后端拿到 code 后向微信接口换取 openid:
# app/services/wx_auth.py import httpx async def get_openid(code: str): appid = config.WX_APPID secret = config.WX_SECRET url = f"https://api.weixin.qq.com/sns/jscode2session?appid={appid}&secret={secret}&js_code={code}&grant_type=authorization_code" async with httpx.AsyncClient() as client: resp = await client.get(url) data = resp.json() if "openid" in data: return data["openid"] return None换取 openid 的接口有频率限制,所以我会在后端做一个简单的内存缓存,同一个 code 只允许换取一次,避免恶意请求刷接口。拿到 openid 后,可以查询用户表确认是否为新用户,新用户自动注册一条记录。
这里要特别提醒:appid 和 secret 绝不能写在小程序前端代码里,必须存在后端环境变量中。一旦泄露,别人可以冒充你的小程序获取用户信息,这是一个严重的安全隐患。
3.4 数据库表结构与模型设计
答疑系统的核心表我设计了三张:用户表、问答历史表、反馈表。用户表字段包括 id、openid、nickname、avatar_url、created_at;问答历史表包括 id、user_id、session_id、question、answer、source(ai/cache/manual)、created_at;反馈表包括 id、history_id、rating(1 或 2)、comment、created_at。
用 SQLAlchemy 定义模型的代码不复杂,关键点在于索引的设计。问答历史表的 user_id 和 created_at 字段要加联合索引,因为用户查看历史的查询条件是“某个用户的所有记录按时间倒序”,没有索引的话数据量一大查询就会变慢。
反馈表的意义容易被低估。AI 回答并不总是正确的,没有反馈机制,你很难知道哪些问题答得不好。我上线后每天会分析 rating 为 1(不认可)的记录,把这些问题的回答结果挑出来人工复核,改进知识库或调整提示词,这是系统能持续变好的关键一环。
4. AI 回答与知识库检索的整体策略
4.1 各种 AI 接入方式的取舍
实现 AI 答疑有三种路线:纯大模型直接回答、知识库增强生成(RAG)、微调专用模型。我最终选了第二种,现在详细说说为什么。
纯大模型直接回答最省事,把问题丢给模型就能得到答案。但有两个致命问题:一是通用大模型对你业务领域的具体知识几乎一无所知,比如它不知道“某门课程期中考试的具体安排”,回答只能是模糊的常识性建议;二是回答内容不可控,可能包含无关信息。答疑场景要求准确、简洁、可溯源,纯模型路线达不到。
微调模型成本太高,需要准备大量高质量的“问题-答案”对,还要算力资源做训练,适合有强大数据积累的大团队。对绝大多数中小型答疑项目,这是杀鸡用牛刀。最合理的路线就是 RAG:用 Embedding 模型把你的知识库文档向量化,提问时检索最相关的片段,把这些片段作为参考资料塞进提示词,让大模型基于这些资料作答。这样做的好处是答案有据可依,还能通过控制检索范围限制模型发挥空间。
4.2 知识库构建:清洗、切片与向量化
知识库的质量直接决定回答质量。原始文档通常是 PDF 或 Word,里面可能夹杂页眉页脚、表格、水印,直接切片会产生大量噪音。我的清洗流程分四步:抽取出纯文本,去掉页眉页脚和水印文字,统一换行和空格,检测并修正乱码内容。
切片策略是 RAG 里的关键细节。切片太小,比如一句话一个 chunk,检索到的内容缺乏上下文,模型看不懂;切片太大,比如整篇文档一个 chunk,检索精度下降、token 消耗暴增。经过多轮测试,我最终把切片的默认值设为 500 个字符,重叠区域为 50 个字符。重叠的意义在于保证一句话不会被两个切片生硬截断,降低信息丢失的概率。对于不同文档类型可以调整参数,比如代码类的知识库可以按函数块切分。
切片完成后,通过 Embedding 接口把每个切片转成向量,存入向量数据库。查询阶段的流程是:把用户问题转成向量,用余弦相似度算法找出最相似的 Top 3 到 Top 5 个切片,把它们与系统提示词拼接。专门的向量数据库能轻松支撑百万级向量检索,小规模项目用轻量级方案就行。
4.3 提示词设计的三个层次
我在实际调试中发现,提示词写得差,再好的知识库也发挥不出来。答疑场景的提示词我分了三层来设计:系统层、示例层、资料层。
系统层提示词定义 AI 的角色和边界,我的模板大致是:
你是一个专业的在线答疑助手,负责回答用户在学习过程中遇到的问题。 请基于以下资料回答问题,回答要简洁、准确、友好。 如果资料中没有明确答案,请如实说明“资料中暂时没有找到相关答案”,不要编造。 回答控制在 200 字以内。示例层是可选的,如果你有知识库外的少量高频问答对,可以作为 few-shot 例子加进去,帮助模型理解“什么样的回答风格是好的”。资料层就是把检索到的切片内容以“参考资料”的形式放在提示词末尾。这三个层次加载同一个请求里,既限制模型的发挥范围,又给它足够的上下文。
调用大模型时还有几个参数值得注意:temperature 设置为 0.2 左右,太接近 1 回答会飘,太接近 0 会显得机械;max_tokens 设为 500,防止冗长答案消耗 token;top_p 配合 temperature 使用,一般保持默认即可。不同的开放平台参数细节有差异,但思路是通用的。
4.4 内容安全与兜底方案
做答疑系统必须面对一个问题:用户可能问超出知识范围甚至不适合回答的问题。我在安全模块做了两层防护。第一层是输入检测,通过敏感词库规则加分类模型的方式判断用户问题是否包含不合适的内容,命中则直接拒绝并返回友好提示。第二层是输出检测,大模型生成的回答同样要过一遍敏感词检测。
兜底方案指的是知识库没有相关答案时的处理逻辑。我的策略是:先看问题是否属于问候语或闲聊,比如“你好”“谢谢”,这种情况不检索知识库,直接让大模型基于通用常识友好回应;如果是正经问题但知识库没命中,就返回“这个问题我暂时无法解答,请稍后咨询人工客服”,并把该问题自动记录到“未命中问题表”,方便管理员每周复盘补充知识库。
5. 上线前必须做的优化与自查清单
5.1 接口并发控制与限流
AI 接口的调用成本不低,如果小程序上线后被恶意刷接口,一天下来账单可能让人崩溃。限流方案我在后端加了一个简单的令牌桶,按用户维度限制每分钟最多 10 次提问,每个 IP 每天最多 200 次提问。实现不复杂,用内存字典就能完成单机限流,多实例部署时把计数放到 Redis 里。
# app/utils/rate_limiter.py import time from collections import defaultdict class RateLimiter: def __init__(self, max_requests, period): self.max_requests = max_requests self.period = period self.records = defaultdict(list) def allow(self, key): now = time.time() self.records[key] = [t for t in self.records[key] if now - t < self.period] if len(self.records[key]) < self.max_requests: self.records[key].append(now) return True return False当然这只是最基础的保护,如果要更严谨,可以引入滑动窗口或分布式限流框架。但实话实说,对于中小型答疑项目,令牌桶加人机验证已经足够挡掉绝大多数恶意流量。
5.2 小程序审核需要注意的坑
小程序审核比普通网页严格得多,我在审核阶段被打回过两次,总结出这几个必须提前处理的点:一是类目选择,答疑问答类一般选“教育-在线教育”或“工具-信息查询”,选错类目可能导致审核拒绝;二是用户协议和隐私政策,小程序内必须能打开这两个文档的页面,讲清楚收集了哪些信息、如何保护;三是内容安全,审核人员会重点测试敏感词拦截功能,如果发现用户能发送并得到不适合的信息,直接拒绝。
还有一条容易被忽略的规则:小程序的 request 合法域名必须 HTTPS,且在微信公众平台后台配置。如果忘了配置,开发工具里调试时能请求成功,但手机上真机预览时会报“不在以下 request 合法域名列表中”。解决方法是先关闭开发工具的域名校验,但发布前一定要把正式域名配好,同时确保证书有效。
5.3 成本控制策略:Token 用量的监控与优化
大模型 API 按 token 计费,日调用量上来后费用不可忽视。我做了三件事来控制成本:缓存热点问题、限制单次生成长度、设置每日预算告警。热点缓存前面说过了,效果最明显,基本能省掉 30% 的 token 消耗。
max_tokens 的设置也很有讲究。太多会让用户等得久、花得多,太少又可能回答不完整。综合各类问题的回答长度分布,我最终把 max_tokens 设在 500。这个值和温度、top_p 等参数配合,既保证了大部分问题能回答完整,又防止回答过于啰嗦。
每日预算告警我采用简单的方式:在存储层记录每日的 token 消耗,每完成一次请求就累加,消耗超过预设阈值时通过邮件或者企业微信机器人通知管理员。管理员收到告警后,可以临时调低单用户限流频率,或者检查是否存在异常调用,避免月底账单失控。
6. 常见问题与排查技巧实录
6.1 小程序真机请求失败
这是新手上线时遇到最多的问题。报错信息五花八门,但本质原因就几类:域名没有配置到微信公众平台的服务器域名列表;域名没有备案或没有 HTTPS;开发工具里勾选了“不校验合法域名”,换到真机就失效。排查思路是先看报错信息里是否包含“url not in domain list”,如果有,就是域名配置问题;如果提示 SSL 证书错误,那就要检查证书链是否完整。
这里需要留意,微信的域名校验是支持端口号的,但只支持 80 和 443,其他端口一律拒绝。如果后端部署在 8000 端口,要么通过 Nginx 反向代理到 443,要么换成 443。我前期为图省事直接用 8000 端口联调,真机测试时吃了不少苦头。
6.2 AI 回答内容截断和乱码
内容截断最常见的原因是 max_tokens 设置太小,模型生成到一半被迫停止,回答停在半句话上。排查方法是在后端日志里打印完整的 model response,看返回的 finish_reason 字段:如果是 length,就是 token 上限问题;如果是 stop,则是正常结束。解决方案有两种:调大 max_tokens,或者在提示词里明确要求“回答控制在 200 字以内”。建议两个都做,前端展示更可控,后端成本也更低。
乱码问题多出在编码格式上。之前一次部署到 Windows 服务器时,Python 读取知识库文件默认使用系统编码,中文全部变成问号。解决方法是打开文件时显式指定 encoding="utf-8",数据库连接也统一设置 charset=utf8mb4。这些坑在本地开发时未必暴露,一旦换环境就冒出来,所以项目从一开始就要把编码写死,别依赖系统默认值。
6.3 历史记录丢失
历史记录丢失去查两个地方:小程序本地存储和后端数据库。本地存储的问题通常是你在 storage 里存了一个超大 JSON 对象,超过 10MB 后写入失败。另一个常见原因是用户更换设备,本地记录不会自动同步。我的做法是本地存储只做展示缓存,每次进入历史页时通过接口拉取后端数据,以服务端数据为准。这样即使本地缓存被清,用户仍然能通过接口看到历史记录。
6.4 知识库检索不到相关内容
检索不到内容,通常是下面几个原因叠加造成的:切片粒度太大导致语义混淆、Embedding 模型处理特定领域词汇的效果不佳、知识库文档本身没有覆盖用户问题。排查时可以先把检索到的切片内容打印出来,看看相似度分数是不是明显偏低。如果分数低,试着把切片的 overlap 调大一些,比如从 50 调到 100;如果检索到了但模型没用好这些内容,那要重点检查提示词里资料层的格式,确保模型能清楚地看到“以下是参考资料”的边界标记。
6.5 热门问题速查表
我把高频排查项整理成一张表,方便你直接对照:
| 症状 | 可能原因 | 解决动作 |
|---|---|---|
| 真机请求报域名错误 | 后台未配置合法域名 | 在小程序管理后台添加 HTTPS 域名 |
| 接口偶发超时 | AI 接口响应慢 | 前端增加重试 + 后端打印耗时日志分析瓶颈 |
| 相同问题回答不一致 | 未设置缓存 / 温度过高 | 加缓存 + 温度调低到 0.2 |
| 知识库答案和问题无关 | 切片粒度不合理 | 调整 chunk_size 和 overlap |
| 用户重复提交 | 前端未做防抖 | 按钮添加 loading 状态并禁用 |
7. 后续扩展方向
第一版稳定运行后,值得做的扩展有这么几个方向:一是提问形式的多模态化,支持拍照提问、语音提问,前者对学习类场景帮助巨大;二是知识库的自动化更新,当问答历史里出现新的高频未命中问题时,自动提醒管理员补充文档,而不是等人发现问题;三是管理面板的完整化,目前知识库的上传和编辑还需要手动操作数据库,可以开发一个简单的 Web 界面,让运营人员自己维护。四是接入人工无缝切换,当 AI 判断无法回答或用户主动发起转人工时,自动创建工单分发给对应客服人员,这就从自动答疑升级成了完整客服系统的核心模块。
从架构角度来说,现在这套代码在扩展这些能力时不需要推翻重来,新的能力只要能对接上现有的服务层和数据库就能平滑融入,所以前期分层的设计省了大量后期重构的时间。
我个人在搭建这套答疑系统时踩过不少坑,最大的体会是:AI 答疑系统的难点从来不只是“调用一个聪明的大模型”,而是知识库怎么组织、答案怎么可控、内容怎么安全合规。你要是自己动手做,建议先把知识库这块做扎实,再花心思调提示词,最后才去考虑花哨的交互。先让 AI 能回答得准,再让它回答得快,这个顺序千万不要颠倒。