商会小程序有一个很反直觉的现象:每天登录的会员很多,但真正打开"找秘书处"入口的会员更多。这些会员问的问题高度重复——会费怎么交、章程在哪下载、本月活动在哪开、我的会员等级是不是到期了。
秘书处两三个干事,每天要花掉大半天回这些重复问题。剩下的时间才用来做真正需要人处理的事:活动对接、跨商会资源撮合、会长临时交办。
我们试过几种方案。第一种是 FAQ 知识库,会员自助查,但实际点击率不到一成,因为大家更习惯问一句"我这个情况怎么办"而不是先去翻列表。第二种是关键词机器人,问题进来后正则匹配,但凡是问题里多两个字、换个说法,就匹配不上。第三种是直接接入大模型 API,但模型不知道会员的实时状态,回答永远是模板化的官腔。
后来我们换了一个思路:让大模型当"调度员",把具体信息查询交给后端去查。这就是下面要写的整套方案。
一、整体架构
整个助手由四块组成。
第一块是通义千问 Qwen,负责理解会员的自然语言问题、判断要不要查后端、决定调哪个接口、把查回来的结果用自然语言回答给会员。这里我们用的是 Qwen-Max,因为会员问题里常带口语化表达,Max 系列的指令遵循更稳。
第二块是阿里云函数计算 FC,承担整个调用入口。FC 跑的是事件驱动的无服务器函数,会员从小程序发一次请求,FC 起一个实例、调 Qwen、必要时再起几个实例并发查后端,处理完一次请求就把实例释放掉。这种"用一次起一次"的模型,对商会这种白天高峰、夜里几乎没量的业务形态特别合适,省了一台常驻服务器的钱。
第三块是百炼 + 自建向量库,承担知识库问答。商会的章程、制度、活动通知、行业报告这些静态资料,先切片、向量化、进百炼的向量库;会员问到具体业务规则时,先走 RAG 召回相关文档片段,再让 Qwen 结合片段回答。这部分比纯靠模型"硬答"靠谱得多,因为模型不会乱编商会的制度条款。
第四块是后端业务系统的查询接口,承担所有"需要会员本人数据"的查询。比如"我这个月会费交了没",模型要把会员编号传给后端,后端去查实际的支付记录、查他今年的会费账单、查他过往的支付习惯。
二、FC 入口函数的核心代码
下面是 FC 入口函数的核心代码。这是整个助手的主流程:解析小程序请求、调 Qwen、判断要不要调工具、调完工具再回 Qwen 拿最终回答。
# fc_handler.py
import json
import dashscope
from dashscope import Generation
from http import HTTPStatus
from tool_dispatcher import dispatch
TOOLS = json.loads(open("tools.json", encoding="utf-8").read())
def handler(event, context):
body = json.loads(event)
question = body.get("question", "")
member_id = body.get("member_id", "")
# 1. 构造系统提示,把会员身份塞进去
system_prompt = (
"你是商会智能助手,会员编号是 " + member_id + "。"
"回答必须基于工具查询结果,不要凭空编造。"
)
messages = [
{
"role": "system", "content": system_prompt},
{
"role": "user", "content": question}
]
# 2. 第一次调 Qwen,让它判断要不要调工具
msg = call_qwen(messages, TOOLS)
# 3. 如果模型说要调工具,循环调,直到它不再要工具
for _ in range(5):
if not msg.get("tool_calls"):
break
for tc in msg["tool_calls"]:
name = tc["function"]["name"]
args = json.loads(tc["function"]["arguments"])
result = dispatch(name, args, member_id)
messages.append({
"role": "tool",
"tool_call_id": tc["id"],
"content": json.dumps(result, ensure_ascii=False)
})
msg = call_qwen(messages, TOOLS)
return {
"statusCode": 200,
"body": json.dumps({
"answer": msg.get("content", "")}, ensure_ascii=False)
}
def call_qwen(messages, tools):
response = Generation.call(
model="qwen-max",
messages=messages,
tools=tools,
result_format="message"
)
if response.status_code == HTTPStatus.OK:
return response.output.choices[0].message
raise RuntimeError("qwen call failed: " + str(response.code))
三、工具定义怎么写
工具定义决定了模型"能调什么"。我们把商会业务里最高频的几类查询抽象成函数,让模型按需调用。
// tools.json
[
{
"type": "function",
"function": {
"name": "get_member_fee",
"description": "查询指定会员的会费账单,传入会员编号和年份,返回应缴、已缴、欠缴金额",
"parameters": {
"type": "object",
"properties": {
"member_id": {
"type": "string"},
"year": {
"type": "integer", "description": "查询年份,缺省为当前年"}
},
"required": ["member_id"]
}
}
},
{
"type": "function",
"function": {
"name": "get_upcoming_activities",
"description": "查询指定商会近期的活动安排",
"parameters": {
"type": "object",
"properties": {
"chamber_id": {
"type": "string"},
"limit": {
"type": "integer", "description": "返回条数,缺省 5"}
}
}
}
},
{
"type": "function",
"function": {
"name": "get_member_level",
"description": "查询会员的等级、有效期、累计缴费记录",
"parameters": {
"type": "object",
"properties": {
"member_id": {
"type": "string"}
},
"required": ["member_id"]
}
}
}
]
工具描述里写清楚"什么时候调"很重要。模型读 description 来决定要不要触发这个工具,description 写错了,模型会乱调或者不调。
四、知识库怎么搭
商会里真正稳定的"长知识"——章程、制度、历史通知——都放在百炼的向量库里。具体做法是每周把商会后台新发的通知、章程更新、新增的行业报告,自动切片、向量化、入库。
会员问"理事会有几票表决权"这种问题,模型先去向量库召回相关章程片段,再结合片段回答。RAG 召回加 Qwen 重写回答,准确率比纯模型答高很多,模型也很少再出现"自己编"的情况。
五、踩过几个坑
第一个坑是上下文管理。模型是有上下文窗口的,会员连续追问几次后 messages 会越来越长,请求会越来越慢。我们的做法是超过 8 轮对话就触发压缩,把早期 messages 总结成一段短文本塞回去,效果很稳。
第二个坑是函数冷启动。FC 长时间不调用会有冷启动,第一次调起来慢。后来我们在 FC 里加了预置并发(Provisioned Concurrency),冷启动基本感受不到了。
第三个坑是限流。Qwen API 本身有限流,商会做活动期间并发问问题,瞬时 QPS 一高就会触发限流。我们的做法是在 FC 入口加一层简单的令牌桶,把瞬时并发压住,平峰再放过去。
六、效果
上线之后,会员的重复问题秘书处基本不需要再接了,平均每天几百次问询绝大多数都让 AI 助手自己处理掉了。成本上看,Qwen API 单次几厘到几分钱,FC 单次几厘,整个助手每天跑下来的成本远低于一个兼职秘书一个月的工资。更关键的是,秘书处的干事终于从"回消息"里解放出来,去做真正需要人际沟通的事。
我们这边落地的商会管理系统叫未来漫城·商会互联平台,上面这套 AI 助手现在已经嵌在小程序的"找秘书"入口,会员随口问一句就能直接拿到自己账单和活动安排,不再需要等干事回消息。
如果你们也在做类似的"垂直行业 AI 助手",这套"通义千问 + FC + 工具调用 + RAG"的组合是一个比较稳的起点,可以直接拿来用再根据自己业务调。