☰
零前端基础:一天给BI系统接入AI智能体,实现自然语言查数
2026/10/16 11:03:28 网站建设 项目流程

成了!不会前端,花一天时间给公司 BI 系统装上了 AI 智能体!

昨天干了件自己都觉得有点离谱的事:一个写了好几年 SQL 和 Python、看到 Vue 就想逃跑的人,硬生生在一天时间里,给公司用了两年多的 BI 系统加装了一个“AI 数据问答助手”。标题里的“AI 智能体”,其实就是让业务同事直接在 BI 系统里用自然语言问数据,比如“上个月华东区的销售额环比变化是多少”,它能自动生成图表、给出结论、还能追着问下一层。

如果你也是后端、数据分析或者运维出身,完全不懂前端,但老板突然甩过来一句“给咱们的报表加个 AI 对话功能”,那我这篇东西大概率能帮你省下几天的踩坑时间。我不会跟你聊什么高深的大模型微调,也不会要你去看 React 文档,我用的方案全部是“会 Python 就能跑”的东西。这篇就记录一下我实际的选型逻辑、完整的接入过程、还有我踩过的那些坑。

1. 先梳理一下:这个智能体到底替我们做了什么

开始动手之前,我花了大概半小时把需求拆清楚。老板说的是“加个 AI 智能体”,但我很清楚,这种需求最忌讳一上来就做一个通用的聊天机器人。如果直接接一个大模型 API,做一个“什么都能聊”的对话框,最后大概率沦为公司的娱乐工具,业务部门问两句发现答非所问,就再也没人用了。

所以我把目标收敛成了三件事:能用自然语言查数、能看懂普通报表指标的口径、能把结果用图表的形态展示出来。说白了,就是要做一个“长在 BI 系统里、张嘴就能查数”的助手,而不是一个万能聊天框。

这个定位决定了后面的所有技术选型。我需要做的不是训练模型,而是搭一条链路:用户输入自然语言 -> 理解意图、结合数据权限映射成 SQL -> 拿到数据 -> 生成结论和图表 -> 返回给用户。这其实就是业界常说的 NL2SQL 加轻量 Agent 的组合,只是我们用现成的大模型 API 和 BI 系统能力,把这个链路快速串起来。

我从一开始就没打算碰 BI 系统前端源码。公司 BI 前端是别人维护的,我不可能改它的代码。好在办法总比困难多:BI 系统大多支持自定义菜单或内嵌页面,我只要把自己做好的网页用 iframe 挂进去,菜单点击进去直接显示,相当于借了 BI 系统的壳,塞进一个独立的子应用。这也是我这种“零前端”的人能一天落地的最大前提。

1.1 为什么敢用“嵌入”而不是重新开发一个前端

很多后端同学一听说要加页面就慌,觉得必须用 Vue、React 重写一个模块。但这件事本质上是“内容提供”而不是“前端开发”。BI 系统本身就是一个容器,它已经处理好了菜单权限、登录态、布局框架,我要做的只是在里面开一个窗口,把自家服务渲染的页面嵌进去。

我选择的方式是:在 BI 系统里新增一个自定义菜单,配置一个 URL,指向我部署的 Python Web 服务。页面内部用现代浏览器天然支持的 iframe 加载,BI 系统点击菜单,实际上就是在主界面中间打开了我那个应用。这种做法有几个明显的好处:我不依赖前端团队排期,不用碰公司前端仓库,改样式不影响 BI 主系统稳定,而且后续哪怕要升级成更复杂的前端框架,只要 URL 不变,整体结构也不需要动。

我用的搭建工具是 Streamlit 和 FastAPI 的组合。Streamlit 负责页面,FastAPI 负责给 BI 系统提供数据查询接口。两者都是 Python 生态,我写的所有代码加起来不超过 400 行。当然有更潮的 Gradio,但 Streamlit 的表格和图表组件在数据场景下更顺手,所以最后选了它。

1.2 这个方案适合什么团队,不适合什么团队

先说说边界,免得你拿着一套方案到处套。如果你们团队里没有任何一个人懂 Python,这套方案跑不起来;如果 BI 系统连“自定义菜单/嵌入页面”这种能力都没有,你也得先费劲找运维开一个反向代理来挂应用。反过来说,如果系统用的是主流开源 BI,或者自研 BI 有扩展位,这方案就非常顺手。

我的环境是:公司 BI 基于开源架构二次开发,数据在 ClickHouse 和数据仓库里,查询层有现成的 SQL 查询网关,但没有对业务开放。我需要先把这个查询网关封装成一个对上游友好的 HTTP 接口,再做后面的事。为了让项目尽量通用,下面所有步骤都不绑定具体 BI 品牌,只要是“BI 系统能提供数据查询 API,且允许自定义菜单”的场景,都可以照做。

2. 工具选型:不会前端,凭什么一天搭出能用的界面

选型是整个项目里最值得写的一段。很多人有个误区,觉得“不会前端 = 不能做带界面的东西”,其实完全错了。Python 生态里有好几个“开箱即用”的 Web 应用框架,本身就是面向非前端工程师设计的。

我对比了三个方案:Gradio、Streamlit、Flask/FastAPI 手搓 HTML 模板。Gradio 的强项是机器学习 demo,交互控件封装得很好,但做数据看板式的界面反而别扭;Streamlit 的图表组件和缓存机制跟 BI 场景天然契合,写起来跟写普通 Python 脚本差不多;而“手搓 HTML 模板”这条路看上去很自由,但你需要自己处理 Bootstrap、CSS 布局、JS 交互,那等于让我这个零基础的人重新学前端,直接排除。

2.1 Streamlit 到底香在哪里

Streamlit 最打动我的一点,是它用“从上到下执行的脚本逻辑”代替了“事件驱动的回调逻辑”。常规前端里,按钮点击、数据加载、页面渲染之间要靠回调函数串联;但 Streamlit 里我只需要写一个从上往下的脚本,用 session_state 记住对话历史,每次用户输入新问题,脚本重新跑一遍,自动更新页面。这对后端思维的人来说几乎是零学习成本。

另外它自带图表渲染。我只需要在代码里拼好 pandas DataFrame,调用 st.bar_chart 或者 st.line_chart,就能出图。BI 场景最常用的就是柱状图、折线图、表格,Streamlit 全都覆盖了。我不用面对 ECharts 的配置项、不用调 CSS,只处理数据本身。

2.2 为什么还要一个 FastAPI 服务

有人说,既然 Streamlit 自己也能处理请求,为什么不全部交给它?这里有个现实问题:BI 系统那边的菜单嵌入,希望访问的是一个稳定、无状态的页面地址;而 Streamlit 是一个偏“应用型”的长连接服务,直接暴露在 BI 系统网络环境下,容易出会话管理问题。更重要的一点是,BI 系统的查询网关需要在一个服务端环境里稳定运行,并且要为未来的 API 调用留一个干净的入口,这个角色 FastAPI 更合适。

所以我让 FastAPI 承担“后端大脑”:负责接收 BI 系统跳转时携带的身份信息,校验权限,调用大模型 API、查询 BI 数据网关,然后把结果以 JSON 返回。Streamlit 只负责“前台界面”:用户在输入框里打字,请求发到 FastAPI,拿到图表数据后渲染。前端嵌入的 URL 也是指向 Streamlit 这个页面地址,本质上页面上每个动作都会变成对后端 FastAPI 的一个请求。

2.3 技术栈最终长这样

最终的技术栈非常简洁:页面层是 Streamlit,网关层是 FastAPI,数据层是 BI 系统自带的 SQL 查询接口,AI 能力来自大模型服务的 API。整个过程我只需要 Python 3.9 以上环境,外加 Streamlit、FastAPI、requests、pandas 几个常用库,没有引入任何需要编译、需要前端打包的组件。

这里想额外强调一个经验:不要为了“显得专业”而引入消息队列、向量数据库这些重组件。单租户内部 BI 场景,数据量撑死几百张表、几十个用户,一个 FastAPI 进程加一个请求级缓存就够了。工具越重,你越不可能一天搞定。

3. 核心实现全过程:从 BI 数据出口到 AI 对话页面

下面这部分是全文的重头戏,我会把每个步骤拆开讲,包括具体代码片段、配置项、以及为什么要这么干。即使你没有完全一样的 BI 系统,照着思路也能迁移到自己的场景。

3.1 第一步:给 BI 系统开一个“对话查询接口”

BI 系统内部通常有非常细的数据权限体系,比如销售只能看自己负责的客户,大区总监能看整个大区。这些权限如果直接透传给大模型,复杂且容易泄露;但不透传,业务人员又只能查到越权数据。我的处理方式是:让 BI 系统自己做权限过滤,而不是让智能体自己拼 SQL。

我在 FastAPI 里写了一个代理接口,它接收三个参数:当前用户的身份标识、查询请求内容(经过脱敏后要查询的指标)、以及可选的过滤条件。然后这个接口以服务账号身份带着用户身份标识去请求 BI 系统的查询网关。BI 系统在底层 SQL 执行时,会根据用户身份自动追加数据权限条件,并把查询结果按标准格式返回。这个设计意味着,任何越权查询都会被 BI 系统自身拦截,AI 智能体就算“胡说”,也骗不过底层权限。

我用一个最简化的代码示意一下这个关键环节:

from fastapi import FastAPI, Depends, HTTPException import requests, os app = FastAPI() def verify_token(token: str): # 调用 BI 系统的校验接口,确认访问来源合法 resp = requests.post( f"{os.getenv('BI_BASE')}/auth/verify", json={"token": token}, timeout=5 ) return resp.json() @app.post("/bi/query") def query_bi(user_token: str, body: dict): ui = verify_token(user_token) if not ui.get("valid"): raise HTTPException(status_code=403, detail="invalid user") # 携带身份标识,交给 BI 查询网关;权限过滤由 BI 完成 sql = body.get("sql") resp = requests.post( f"{os.getenv('BI_BASE')}/query-gateway", headers={"X-User-ID": ui["user_id"]}, json={"sql": sql, "params": body.get("params", {})}, timeout=60 ) return resp.json()

整个上午的两个小时我基本都花在沟通这个接口的数据格式上。我建议你在动手前,先看一眼 BI 系统现有查询日志里返回结果的数据结构,八成就是你需要对接的那个形状。别自己定义一套“理想格式”然后硬做适配层,能少掉很多眼泪。

3.2 第二步:让大模型学会“看懂表结构并输出 SQL”

这一步是整个智能体的灵魂:把用户说的话翻译成 SQL。我用了最实用的方式——告诉大模型每一张表的结构、字段含义和常用的口径规则,让它基于这些信息生成 SQL,而不是真的让它去“学习”数据库。

这个方案的执行成本极低,关键是写好 Prompt。我自己总结了一个模板,基本格式如下:

  1. 系统角色设定:你是数据分析师,只能基于提供的表结构生成 SQL。
  2. 表结构清单:列出表名、字段名、字段类型、字段业务含义。
  3. 口径规则:比如“销售额 = 订单金额 - 退款金额”“环比 = (本期-上期)/上期”。
  4. 输出要求:只输出 SQL 本身,不允许解释。
  5. 安全约束:禁止使用 DELETE、UPDATE、DROP 等危险语句,只允许 SELECT。

有了这个基础 Prompt,再配合少量示例(我准备了 5 条典型问答作为 few-shot),准确率基本能到 80% 以上。剩下 20% 的错误可以靠“第二轮修正”:把 BI 系统报错信息回传给大模型,让它根据报错调整 SQL。这两步组合起来,实际场景里的可用性已经相当高了。

关键代码像这样:

from openai import OpenAI # 如果是 API 兼容协议的服务 client = OpenAI(base_url=os.getenv("LLM_BASE"), api_key=os.getenv("LLM_API_KEY")) SCHEMA_PROMPT = """ 你是企业数据分析助手。当前数据库包含如下表: table:sales_summary - dt: 日期,格式YYYY-MM-DD - region: 区域名称 - amount: 销售额(已扣退款) - refun: 退款金额 业务口径:销售额=订单金额-退款金额;环比=(本期-上期)/上期。 请把用户问题转换为SQL,只能返回SQL语句。用户问题:{question} """ def ask_llm(question: str) -> str: resp = client.chat.completions.create( model=os.getenv("LLM_MODEL"), messages=[ {"role": "system", "content": SCHEMA_PROMPT.format(question=question)}, ], temperature=0, ) return resp.choices[0].message.content.strip()

温度设成 0 非常重要。数据分析场景不允许模型自由发挥,稍微带点“灵感”SQL 就容易跑偏。另外需要注意,这里我直接展示的是单轮生成,实际项目里我加了一层“验证器”,执行前先检查是否包含 SELECT 关键字且不包含危险语句,这是最后一道保险丝。

3.3 第三步:把前端页面拼出来并挂进 BI 系统

前端页面我交给 Streamlit。它的开发模式非常“脚本化”:从上到下执行,每次用户操作后重跑。这意味着对话历史要放在 st.session_state 里,避免重跑时丢失。

页面结构分成四个区域:对话框、历史消息列表、快速提问按钮组、以及查询结果图表区。核心交互逻辑是:用户输入一句自然语言 -> 前端把这句话发给 FastAPI 的 /agent/query 接口 -> 后端返回“SQL、数据表格、结论文本、图表配置”四件套 -> 前端直接用原生组件渲染。

Streamlit 的代码大概长这样:

import streamlit as st import requests import pandas as pd st.set_page_config(page_title="AI 数据助手", layout="wide") if "history" not in st.session_state: st.session_state.history = [] st.title("BI 智能问答助手") question = st.chat_input("输入你的数据问题,例如:上个月华东区销量 Top10 产品") if question: resp = requests.post( "http://localhost:8000/agent/query", json={"user_token": st.session_state.get("token"), "question": question}, timeout=30, ) data = resp.json() st.session_state.history.append({"q": question, "a": data}) for item in st.session_state.history: st.markdown(f"**问:{item['q']}**") st.markdown(f"{item['a']['conclusion']}") if item["a"]["columns"]: df = pd.DataFrame(item["a"]["rows"], columns=item["a"]["columns"]) st.dataframe(df) st.bar_chart(df)

为了嵌入 BI,我只做了一件“不太光彩但有效”的事:让页面接受一个 token 参数,这个 token 由 BI 系统在菜单跳转时通过 URL 拼接传入。BI 系统里配置菜单地址为http://agent.server/?token={用户会话标识},Streamlit 那边从 query 参数里取 token,后续请求都带上。这样我就不用处理 BI 的登录态校准,等于把认证代理给 BI 系统了。

3.4 第四步:返回结果的包装与展示

用户真正在乎的不是 SQL,而是结果能看懂。所以后端从 BI 查询完数据后,我让大模型做了一次“结论总结”。就是让模型基于返回的数据表格和用户问题,生成一段不超过 80 字的中文结论,比如“华东区 7 月销售额 2300 万,环比上涨 12%,主要由上海和杭州拉动。”

这样做加不了多少延迟,但体验完全不一样:用户第一眼看到的是结论,而不是一张冷冰冰的报表。你可以在总结 Prompt 里加一行“如果涨幅超过 10%,给出业务解释建议”,马上又有了“洞察”感。这一步属于锦上添花,但对业务的冲击力最大,很多领导觉得“AI 有了”,其实就是因为这一步。

4. 实操中躲过的那些坑,每个都是花钱买来的

4.1 第一个大坑:跨域和 Cookie 把我折腾到下午三点

最初我把 Streamlit 和 FastAPI 都部署在同一台机器上,Streamlit 页面在 8501 端口,FastAPI 在 8000 端口,前端直接 fetch 请求后端。Streamlit 同源策略下,前端请求后端口完全不一样,直接就被浏览器拦了,CORS 报错红了一片。

解决办法是在 FastAPI 里增加 CORS 中间件,把 Streamlit 的域名来源加白名单。这个操作很简单,但要在启动时做,而且我后来发现不能只加一个allow_origins=["*"],因为带着 BI 系统的 Cookie 请求时,通配符不允许携带凭证。最后我用的是显式来源列表:

from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=[os.getenv("AGENT_ORIGIN")], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )

不过这里还有个潜藏的坑:BI 系统内部如果本身也嵌了 iframe,登录态 Cookie 往往带SameSite属性限制。如果你发现页面打开但请求一直 401,大概率是 Cookie 在 iframe 场景下没有发送。处理方式是让 BI 管理员把该站点的 Cookie 设为SameSite=None; Secure,或者更简单,干脆别用 Cookie 传递身份,改成上边说的 URL token 参数。我最终选的是 URL token,省了一堆麻烦。

4.2 第二个大坑:大模型生成的 SQL 里,WHERE 条件夹带私货

模型虽然被限制了只允许 SELECT,但它的“智能”有时候是灾难。比如用户问“上海大区的数据”,模型可能自作聪明地把region='上海市'写进 SQL,但数据库里的字段值是上海,结果查出空数据。这不是 SQL 语法错误,而是语义匹配问题——BI 底层的维度字典里各区域写法不统一。

我的对策是:在 Prompt 的表结构描述里给出“字段枚举值示例”。也就是在建表描述里写清楚:region 字段可能取值:华东、华北、上海、北京...,并要求模型优先使用枚举值,若不确定则不要加 WHERE 条件,而是让结果集返回后由后续流程过滤。这样一来,模型犯错的概率大幅降低,即使出问题了,也更容易DEBUG。

4.3 第三个大坑:报表口径千奇百怪,一个 Prompt 永远不够

原本以为只要表结构清晰就够了,实际上同事问的很多问题涉及到复杂的业务口径。比如“销售目标达成率”,这个指标在业务里可能是“已发货金额除以目标金额”,也可能是“回款金额除以目标金额”,两个口径结果差得远。如果模型不知道选哪个,结果看起来合理,实际上根本错到离谱。

所以我把“口径规则”提升为 Prompt 的最高优先级,并把常用指标公式单独固化到一份配置文件里,随 Prompt 注入。一旦业务方又改了定义,我只需要改配置文件,不用改代码。这个做法也让我在面对业务质问时,能快速回答:“你告诉我的口径写错了,我改一下就好。”

还有一个经验:不要指望模型自己分辨哪个口径是对的。你要在 Prompt 里明确写“如果用户问题涉及指标 X,必须使用口径 Y;如果存在多口径,先向用户确认”。

5. 常见问题速查表

这部分我把半天内被同事问得最多、以及我自己排查过的问题整理成了表格,方便你对照处理。

问题现象可能原因解决方案
页面打开一片空白iframe 被 BI 系统的 X-Frame-Options 拦了在 Streamlit/FastAPI 响应头里放行X-Frame-Options: ALLOW-FROM或配置frame-ancestors
能打开页面但接口 401跨域下 Cookie 没带改走 URL token 参数方式,不用 Cookie
模型返回的 SQL 字段不存在Prompt 里的表结构未更新建立表结构配置,每次改表主动同步到 Prompt
查询结果权限越界没有经过 BI 网关所有查询统一走“携带用户身份”的 BI 查询接口
结果图全部叠在一起图表用错了维度字段检查返回的 DataFrame 里是否有需转为字符串的日期/区域字段
并发一多就超时大模型接口串行调用给 /agent/query 加线程池;引入简单的内存缓存
提示“未授权”但权限没问题菜单跳转 URL 拼接的 token 被 URL 编码在 Streamlit 侧做一次 URL decode 再使用

这里多说两句缓存。我给“相同问题+相同用户”做了 10 分钟的内存缓存,命中后不再调用大模型和 BI 网关。内部工具这层优化非常管用,因为业务顾问对同一类问题的重复查询率很高。内存缓存只适合单实例部署,如果你要横向扩容,就得换 Redis,但就这个场景来说,内存就够了。

6. 用下来的一些真心话

功能上线后,我没急着公布,先在数据分析组内部试了两天。一个很有意思的观察:数据分析师对 AI 输出 SQL 的质量挑刺最多,但业务同事反而普遍觉得“好用”,因为业务根本不管 SQL 对不对,只要结果和口径对得上,他们就觉得“这玩意儿成精了”。

我后续做了一次小迭代,效果最明显:在问答框下面加了“常用问题”快捷入口。点一下“本月各区域目标完成率”,自动带出问题并触发回答。这一步让那些平时不敢打字、表达能力一般的人也能瞬间上手。别小看这个设计,上线后使用率直接翻倍。

关于安全,我也补了一句明确的底线:所有生成 SQL 都要经过 BI 网关执行,业务人员的任何追问都不会直接触达原始数据库,这样即使模型被恶意 Prompt 攻击,也只是影响查询结果,不会威胁数据安全。这也是为什么我始终坚持让“大模型只生成 SQL、BI 系统执行 SQL”,而不是“让大模型展示数据”,职责分离,出事后也更容易复盘源头。

如果你也想在内部系统里做类似的东西,我最后的建议是:不要一开始追求“完美 Agent”。对话历史先限定一轮追问就好,SQL 生成正确率到 80% 就先上线让业务试,图表先做柱状和表格两种形态,功能稳定后加“洞察”、加“数据预警”、加“维表规范化”,有的是进化空间。先解决不会前端的问题,你会发现后面所有问题都不再是“能不能做”,而是“想不想做得更好”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询