用数据讲述中国古桥:从信息架构到 AI 对话的全链路实践¶
原标题:从零到上线——我如何用 Vue + ECharts + FastAPI 构建了一个古桥时空图谱
一句话介绍:一个支持地图下钻、多维联动筛选、AI 问答的中国古桥数字档案系统,收录了 100 座古桥、121 张实景图片,覆盖 22 个省级行政区。
深度专题:AI 对话系统深度解析 | 视觉设计系统
为什么做这个¶
我是数据科学与大数据技术专业大三学生,这是我参加 2026 年(第 19 届)中国大学生计算机设计大赛"信息可视化设计——交互信息设计"赛道的作品。
选题的出发点很简单——中国古桥是中华文明物质遗产的重要组成部分,但网上的信息要么是百科词条,要么是零散的旅游攻略,没有一个地方能让我同时看到"这座桥在哪、什么年代建的、什么结构、有什么故事"。我想做的不是"桥梁知识百科网页",而是一个具有时空结构和文化表达的交互探索系统——用户可以在其中从全国格局一路下钻到单桥证据。
信息架构:"时域形义"四维框架¶
在动手写代码之前,我花了不少时间想清楚一个问题:100 座桥的信息应该用什么逻辑组织起来?
最终我选择了"时—域—形—义"四个维度作为整套系统的信息骨架:
| 维度 | 含义 | 在系统中怎么体现 |
|---|---|---|
| 时 | 历史时期 | 时期筛选器、时期趋势图、详情页时期标签 |
| 域 | 空间分布 | 中国地图下钻、省域聚合、空间散点 |
| 形 | 结构类型 | 桥型筛选器、桥型分布图、5 类桥型 AI 结构图鉴(每类含素描示意图 + 3 条力学/美学解读,详见 视觉设计系统) |
| 义 | 人文记忆 | 诗桥轮播(用 export_bridge_poetry.py 从桥梁档案中提取古诗词,首页横向滚动展示)、文化线索深描 |
其中"时"和"域"做主导航,用户主要通过时间和空间进入探索;"形"和"义"做辅助筛选与解释层,用于丰富展示和深化理解。这个框架贯穿了首页总览→探索工作台→案例详情页的三层递进:
- 宏观层(首页):建立整体认知,统计指标 + 核心案例入口
- 中观层(探索工作台):精准定位,地图下钻 + 多维筛选 + 图表联动
- 微观层(案例详情页):深度理解,"图证 + 解释 + 档案"并行的证据型深描
一个设计取舍
最初方案里有 5 层页面(首页 → 探索 → 省份下钻 → 桥梁档案 → 重点桥详情),后来我把"省份下钻"合并进了探索工作台的地图交互,"桥梁档案"和"重点桥详情"合并为一个参数化路由 /bridge/:id。从 5 层砍到 3 层,页面更少但交互更连贯,开发和维护成本也降了一半。
整体技术架构¶
flowchart TB
subgraph 数据层
A["原始JSON\n100座古桥档案"] --> C["Python ETL 脚本\n15个"]
B["图片CSV清单\n121张实景图"] --> C
C --> D["dashboard-data.json\n聚合数据包"]
end
subgraph 服务层
E["FastAPI 后端\n10个RESTful API"]
F["MCP 联网搜索\nstdio协议"]
E <--> F
end
subgraph 展示层
G["Vue 3 + ECharts\n25个组件"]
G --> H["首页总览"]
G --> I["探索工作台"]
G --> J["案例详情页"]
end
D --> G
D --> E
E <--> G
核心决策:为什么不用数据库?¶
这个问题我纠结了很久。100 座桥的数据量并不大,如果上 MySQL 或 MongoDB,反而引入了额外的运维成本。最终我选择了 JSON 文件 + 内存缓存 的方案:
- Python 脚本预先把原始数据清洗、聚合成一个
dashboard-data.json - 后端启动时一次性加载到内存,用
@lru_cache缓存,所有查询都是内存操作,毫秒级响应 - 前端也能直接读这个 JSON,不依赖后端就能跑起来
方案的好处是部署极简(不需要装数据库),坏处是数据更新需要重新跑构建脚本。对于一个比赛项目来说,这个取舍是合理的——项目汇总版思路说明里原话就写了"如果数据规模控制在 100 条左右,暂时不需要引入数据库系统"。
前后端技术栈选择¶
| 层级 | 技术 | 选择理由 |
|---|---|---|
| 前端框架 | Vue 3 + Vite 6 | 组合式 API(Composition API)天然适合复杂交互状态管理 |
| 数据可视化 | ECharts 5 | 原生支持中国地图组件和交互下钻,省去自己画地图 |
| 后端 | FastAPI 0.115 | Python 异步框架,自动生成 API 文档,跟数据处理脚本同语言 |
| 安全渲染 | markdown-it + DOMPurify | AI 回答需要渲染 Markdown,但必须防 XSS 攻击 |
| 联网搜索 | MCP 协议 | MCP(Model Context Protocol)标准化接入本地搜索服务 |
技术选型的另一个背景
早期方案讨论过用 Dash/Streamlit 这种纯 Python 方案快速出原型,后来考虑到地图下钻动画、页面转场和高度定制的视觉风格,最终选了 Vue + ECharts 的前后端分离方案。虽然开发量更大,但在"作品感"上确实拉开了差距。
水墨主题色板¶
所有 ECharts 图表不使用官方默认色板,而是通过 chartTheme.js 统一配置了一套"古纸水墨"色系——Bridge Brown #6a4735(桥木褐)、River Blue #4f7681(河川蓝)、Earth Orange #c57b3a(土壤橙),地图底色为暖纸色 #f7eee2。所有 tooltip 用古纸底 + 暖色阴影 + 圆角,让图表看起来像旧纸上的舆图而非冰冷的 BI 仪表盘。详见 视觉设计系统。
三个最有挑战的技术点¶
难点一:12 个面板如何联动不卡?¶
问题:探索工作台有 4 个筛选器、6 类图表、1 个地图、1 个列表,一共 12 个交互面板。用户改了任何一个筛选条件,其他 11 个面板都要同步更新。最初的实现直接就卡了。
思考过程:一开始我想用事件总线(EventBus),让每个面板都监听筛选事件,但这样会导致事件风暴——一次筛选触发 11 次事件,每次事件又可能触发后续联动,很容易出循环依赖。
最终方案:利用 Vue 3 的 ref + computed 构建 单一数据源 → 派生计算 的响应式链条:
// 伪代码示意
const province = ref(null) // 筛选状态
const period = ref(null)
const bridgeType = ref(null)
// 所有面板通过 computed 自动订阅
const filteredBridges = computed(() => {
return allBridges.filter(b =>
(!province.value || b.province === province.value) &&
(!period.value || b.period === period.value) &&
(!bridgeType.value || b.type === bridgeType.value)
)
})
筛选状态只有一份,所有下游面板都是"被动计算",不存在事件打架的问题。Vue 的响应式系统会自动做依赖追踪和批量更新,性能也比手动管理好得多。
一个附加的细节:LinkedFilterSelect 组件(9.4KB,不是一个简单的 <select>)实现了选项数量的实时统计——选了"浙江"之后,时期筛选器里每个选项会显示"浙江有多少座这个时期的桥"。这个计数值也是 computed,是对 filteredBridges 做二次分组聚合得来的,帮助用户判断"选了这个选项后还有多少结果"。
难点二:地图下钻的双向同步¶
问题:中国地图支持点击省份"下钻"到省域视图,但地图旁边还有一个省份下拉筛选器。用户既可以点地图选省,也可以用下拉框选省,两条路径必须完全一致。
思考过程:如果地图和筛选器各自维护自己的选中状态,就很容易出现"地图选了浙江,但筛选器还显示全国"的 bug。这个方案在项目早期思路文档里就被讨论过——最终确定采用"地图高亮点击为主,侧边栏筛选为辅"的混合交互方案。
最终方案:让地图和筛选器共享同一个 ref 变量。地图的 click 事件修改这个变量,筛选器的 change 事件也修改这个变量,两者都通过 watch 监听同一个状态去更新视图。这不是什么高深的技术,但在实际实现中非常容易被忽略。
改完之后的连带效果:散点图只显示当前省的桥梁点位,排名图自动切换为省域统计,时期趋势图也同步更新——因为它们都从同一个筛选状态派生。
后端的 filter_bridges() 函数还维护了两张完整的别名映射表——PROVINCE_ALIAS_MAP 覆盖了全部 34 个省级行政区的简称、全称和方言称法(如"晋"→"山西"、"鄂"→"湖北"),PERIOD_ALIAS_MAP 覆盖了中国主要历史朝代的常见别名(如"唐代"→"隋唐"、"北宋"/"南宋"→"宋代"、"两汉"→"秦汉")。无论用户用什么方式表达,系统都能收敛到正确的筛选值。
难点三:大模型"乱说"怎么办¶
问题:接入大语言模型(DeepSeek)做古桥问答后发现一个严重问题——模型会"编造"不存在的古桥数据,或者把甲桥的建造年代安到乙桥头上。
思考过程:通用大模型对这种垂直领域的知识掌握不准确是正常的,核心思路是 约束模型只能用我提供的数据来回答。
最终方案:
- 数据注入上下文:每次对话请求,自动根据用户所在页面类型(首页/探索/详情)构建不同粒度的上下文——详情页注入该桥完整档案 + 同省相关桥梁,探索页注入当前筛选结果摘要,首页注入全库统计概览。要求模型"仅基于以下资料回答"
- 来源过滤:联网搜索环节用
web_source_filters.py中的可信来源三级分类(11KB / 424 行的过滤规则)——权威来源(.gov.cn/.edu.cn/.museum)、二级来源(百科 / 学术平台)、封锁来源(知乎 / 贴吧 / 抖音等 UGC),每条搜索结果按来源层级 + 桥梁术语命中数 + 筛选条件匹配度综合评分 - 双重解析:自然语言筛选请求先用正则表达式提取"浙江""宋代"这类确定性实体,提取不到的部分再交给模型做语义理解;LLM 返回的结果还要经过归一化校验——如果用户未显式提及桥型但模型自动补了,会被主动丢弃
- 候选收敛:每次回答前先在本地 100 座桥中做 name → descriptor → semantic 三级候选匹配,只有候选数精准(1~N 条)时才触发 MCP 联网补充;零候选或候选过多都不联网。这个机制把联网搜索当作增强手段而非默认行为
- 格式约束 prompt:system prompt 中定义了 14 条排版规则——只允许段落/列表/引用/粗体/高亮 5 种 Markdown 语法,粗体仅用于短实体(桥名/朝代/术语,不超过 18 字符),高亮每次回答最多 2 处,禁止整句加粗和标题
- 回答后处理管线:模型输出经过 3 层后处理——去除标题符号、超长粗体自动去除、高亮限额强制执行,连续段落自动拆为分点列表。无论模型输出什么格式,前端拿到的始终是"(可选寒暄)+ 分点列表"的统一结构
- 密钥收敛:所有模型调用全部收敛到服务端,前端不接触 API Key,也不暴露任何 MCP/AI 配置入口
深度阅读
以上 7 点只是摘要。完整的 AI 对话系统架构拆解——包括意图路由决策树、三级候选匹配算法、MCP 联网搜索协议、Prompt 14 条约束逐条解释、后处理管线代码走读——请参阅 AI 对话系统深度解析。
一个真实的安全教训
最开始我把大模型的 API Key 放在前端环境变量里直接调用。后来被指出这是一个安全隐患——前端代码是公开的,Key 会泄露(实际上确实泄露过一次,紧急轮换了密钥)。这件事让我深刻理解了"密钥只保存在服务器 .env"这个原则。
数据处理管线(ETL)¶
这是我在整个项目中最熟悉的部分。整条管线涉及 15 个 Python 脚本,数据从采集到可用经过 5 个阶段:
flowchart LR
A["1. 数据采集\n多源半自动"] --> B["2. 清洗增强\n字段补全+二次丰富"]
B --> C["3. 图片治理\n6步管线"]
C --> D["4. 聚合构建\n★核心脚本"]
D --> E["5. 双模式消费\n静态/API"]
| 阶段 | 做了什么 | 关键脚本 |
|---|---|---|
| 数据采集 | 多来源收集古桥信息,半自动 + 人工校验 | bridge_image_crawler.py |
| 清洗增强 | 字段补全、二次丰富(工程参数+文化线索) | augment_bridge_workbook.py、second_pass_bridge_enrich.py |
| 图片治理 | 多来源图片统一治理:采集→校验→精选→审核 | 6 个脚本协同 |
| 聚合构建 | 合并 JSON + CSV,生成前端可用的聚合数据包 | build_dashboard_data.py ★ |
| 双模式消费 | 静态读取 or API 查询,自动切换 | dashboardData.js |
图片治理:最耗时间的环节¶
100 座桥、121 张图,来源包括网络采集、官方渠道和人工补充。这三类来源的命名规则、分辨率、元数据格式完全不同。我设计了一套多轮管线:
种子采集 → 官方渠道补充 → 人工校验同步 → 精选筛选 → 审核报告 → 运行时目录构建
最终每张图片都记录了归属桥梁、图片类型(主视图/远景/细节/碑刻等)和排序权重,输出为 bridge_images_program_ready.csv。前端的 BridgeImage 组件还内建了图片加载失败的占位图降级逻辑——如果某座桥的图片没找到,不会白屏,而是显示一个桥型轮廓的占位图。
核心构建脚本¶
build_dashboard_data.py 是整条链的"汇合口"——它把 bridges_program_ready.json(100 座桥的主档案)和 bridge_images_program_ready.csv(图片清单)合并,输出一个 dashboard-data.json,包含桥梁列表、统计总览、省域聚合、筛选选项等所有前端需要的数据。运行一次 npm run data:build 就能完成整个构建。
设计决策复盘:双模式架构¶
这是我印象最深的一个设计决策。
最早的版本只有 API 模式——前端调后端,后端查数据。但有一次在没网的教室里演示,页面一片空白。这让我意识到一个问题:比赛评审的网络环境是不可控的。
于是我加了"静态模式":前端直接读 dashboard-data.json,所有筛选、排序、统计逻辑在前端用纯 JavaScript 实现。两种模式通过服务层(dashboardData.js,264 行)统一接口,页面组件完全不需要知道当前跑的是哪种模式:
静态模式下的前端搜索也不是简单的字符串匹配——dashboardData.js 内置了一张语义扩展词表(SEMANTIC_QUERY_TERMS:"跨海"/"风雨桥"/"古渡"/"修缮"/"诗文"/"典故"等),splitQueryTokens() 会把用户输入拆分为多级 token 做组合匹配。在不依赖后端的情况下也能处理"语义相近"的查询。
这个双模式设计实现了体验下限与能力上限的双重保障:
- 静态模式保障了无后端环境下的完整展示能力,适用于脱机演示、竞赛评审
- API 模式在此基础上叠加动态查询与智能问答,适用于在线服务场景
部署:从本地到线上的真实经历¶
系统部署在阿里云香港 ECS(Ubuntu 22.04, 2 vCPU / 4 GiB)上,采用 Nginx + systemd 单机部署方案:
浏览器请求 → Nginx (80端口)
├── / → 前端 dist 静态资源
├── /assets/bridge_images/ → 桥梁实景图片
└── /api/* → 反向代理至 FastAPI (127.0.0.1:8000)
部署中印象最深的一个坑
上线后发现 AI 联网搜索功能一直显示 searchBackend=disabled,排查了半天 MCP 配置,最后发现根因不是代码问题——是之前手动启动 uvicorn 测试时的旧进程占住了 8000 端口,systemd 启动的新进程绑定失败但没报错,Nginx 一直把请求转发到那个不加载 .env 的旧进程上。用 ss -ltnp | grep ':8000' 对比 PID 才发现问题。这件事让我养成了一个习惯:每次更新后必须检查 /api/health 和 /api/ai/config。
选择香港节点而不是内地节点,是因为比赛阶段来不及走域名备案。香港 ECS 可以直接通过公网 IP 访问,比"本地电脑 + 内网穿透"稳定得多。
项目规模量化¶
| 维度 | 规模 |
|---|---|
| 前端 | 4 个页面 + 25 个组件 + 6 类 ECharts 图表 |
| 后端 | 1 个 FastAPI 主模块(1662 行 / 94 个函数),10 个 RESTful API 分三类:数据查询(4)、AI 对话(3)、系统运维(3) |
| 数据处理 | 15 个 Python ETL 脚本,从采集到消费覆盖 5 个阶段 |
| 样式系统 | 8145 行 CSS,统一"古纸水墨"视觉语言 + 自定义 ECharts 主题 |
| AI 对话 | 14 条 system prompt 约束 + 3 级候选收敛 + 3 步回答后处理 + 11KB 可信来源过滤规则 |
| 数据规模 | 100 座古桥 x 22 省 x 121 张实景图 x 6 个历史时期 x 5 种桥型 |
现在的状态和后续计划¶
系统已经部署上线,目前的规模:
- 100 座古桥主样本库,18 个核心深度案例
- 22 个省级行政区覆盖,121 张实景图片
- 3 个主页面 + 1 个方法论说明页 + 10 个 RESTful API + 6 类交互图表
- 地图下钻 + 多维联动筛选 + AI 智能问答
- 数据从 100 座扩展到更大规模
- 移动端适配优化
- 接入域名和 HTTPS
比赛还在进行中,线上版本持续迭代。
写在最后¶
做这个项目最大的感受是:数据的"最后一公里"比想象中难太多了。采集 100 座桥的数据不算难,但要把它们清洗成一致的格式、配上对的图片、组织成合理的信息层次,最后还要让用户在界面上"顺着思路"找到想看的东西——这个过程让我对"数据产品"这四个字有了完全不同的理解。回头看,早期花在信息架构设计上的时间("时域形义"框架、三层递进逻辑、五层砍到三层的页面结构),是整个项目最值得的投入。
如果用一句话总结这个项目的技术关键词:"结构化数据 x 受控 AI x 无损降级"——用信息架构让 100 座桥的混杂数据各就各位,用 prompt 约束、候选收敛和排版后处理让 AI 只说它该说的话,用双模式架构保证任何网络环境下都能完整运行。
延伸阅读
- AI 对话系统深度解析:意图路由、三级候选收敛、联网搜索过滤、Prompt 14 条约束、回答后处理管线
- 视觉设计系统:古纸水墨色板、字体降级链、Surface 面板系统、桥型 AI 图鉴、交互微动效