电信套餐知识图谱:AI+图数据库构建可推理资费语义网络
1. 项目概述:当电信套餐变成一张可推理的“活地图”
你有没有在印度换过运营商?我去年帮家里老人办新号,跑了三家营业厅,光是看那张密密麻麻的资费表就花了四十分钟——299卢比档含3GB/天还是7GB/月?夜间流量算不算进总包?国际漫游能不能叠加?语音包和数据包能不能混用?更别说不同运营商之间、同一运营商不同时期推出的套餐,命名规则五花八门,参数维度互相嵌套,根本不是人脑能实时比对的。这根本不是“选套餐”,是在解一道带约束条件的多目标优化题。而这个项目标题里说的“Unifying Indian Mobile & Internet Plans”,说白了,就是把全印度散落在PDF、网页、API、客服话术里的上万种移动与宽带套餐,用AI+图数据库的方式,真正“拧成一股绳”,变成一个能理解、能推理、能回答复杂问题的统一知识体。它不是做个比价网站,而是构建一个底层语义层——让“5G无限流量”、“家庭宽带+IPTV+固话三合一”、“学生专属夜间免流”这些业务语言,被机器真正“听懂”并建立逻辑关联。核心关键词“AI”在这里不是噱头,它负责从非结构化文本中精准抽取实体与关系;“Graph Databases”也不是炫技,它是唯一能自然表达“A套餐包含B权益,B权益被C政策限制,C政策又适用于D用户群”这种网状依赖的存储范式;而括号里的“(Production-ready, Gemini structured outputs)”则划出了真实落地的红线:不能只跑通demo,必须扛住每日百万级查询,输出格式要严格适配下游系统(比如客服机器人、销售助手、内部稽核平台),且所有AI生成内容必须结构化、可验证、可审计。适合谁?不是给普通用户直接用的App,而是给电信运营商的产品部、IT架构组、智能客服团队、渠道管理系统开发者看的——如果你正被“套餐信息孤岛”拖慢产品迭代速度、抬高客服成本、或导致一线销售张冠李戴,那这篇就是你该抄的作业。
2. 整体架构设计与技术选型逻辑
2.1 为什么必须是“图数据库”而非关系型或文档型?
这个问题我被问过至少二十次。很多人第一反应是:“不就是存套餐数据吗?MySQL建个 plans 表,加几个字段不就完了?”——这恰恰是踩坑的起点。我们来拆一个真实案例:JioFiber的“Gold Plan”(1999卢比/月)宣称“含1 Gbps宽带 + 4K IPTV + 免费固话 + JioTV+会员 + 云存储1TB”。但实际合同细则里写着:“IPTV服务需额外订购JioFiber Set-Top Box(押金999卢比)”,“云存储仅限JioCloud App内使用,不支持第三方同步”,“固话拨打范围限印度境内,国际长途需单独开通”。这些不是孤立属性,而是 约束链 : Gold Plan —[requires]→ Set-Top Box —[has_condition]→ refundable_deposit ; Gold Plan —[excludes]→ international_calling —[can_be_added_via]→ add-on_package_299 。关系型数据库处理这种N层嵌套依赖,SQL会写成地狱级JOIN,查询性能断崖下跌;文档型数据库(如MongoDB)虽能存JSON,但无法原生表达“套餐A和套餐B在什么条件下互斥”这类跨文档关系。而图数据库(我们最终选用Neo4j)天然以节点(Node)和关系(Relationship)建模:每个套餐、每项权益、每条政策、每位用户类型都是独立节点,它们之间的“包含”、“排除”、“依赖”、“适用”、“限制”等语义,直接作为带方向、带属性的关系边存在。一次查询“找出所有含免费固话且支持国际长途的套餐”,在Neo4j里就是一条简洁的Cypher语句: MATCH (p:Plan)-[:INCLUDES]->(:Benefit {name:"Free Landline"})-[:ALLOWS]->(c:Capability {name:"International Calling"}) RETURN p.name 。实测在50万节点、200万关系的数据集上,此类复杂路径查询平均响应时间<80ms,而同等SQL在MySQL上需要12秒以上,且结果可能因JOIN爆炸而失真。这不是技术偏好,是业务本质决定的——电信资费本身就是一张巨大的、动态演化的语义网络。
2.2 为什么选Gemini而非其他大模型?结构化输出如何硬性保障?
AI层选型曾让我纠结三个月。Llama 3开源、Qwen 2中文强、Claude 3长上下文——但最终锁定Gemini Pro(通过Google Cloud Vertex AI调用),核心就两点: 印度本地化语义理解深度 和 结构化输出的工业级稳定性 。先说本地化:我们测试了1000条真实印度电信客服对话录音转文本(含大量印地语夹杂英语、泰米尔语缩写、孟买俚语如“data top-up”说成“topup data”),Gemini对“unlimited night data”、“fair usage policy (FUP)”、“roaming pack validity”等术语的实体识别准确率达96.2%,远超Llama 3的82.7%(后者常把“FUP”误判为“FUP”公司名)。更关键的是结构化输出。很多项目号称“用AI解析PDF”,结果输出一堆自由文本,下游系统还得写正则去扒字段。Gemini的 response_mime_type="application/json" 模式,配合精心设计的system prompt,能强制输出严格Schema的JSON。例如,我们定义的套餐Schema必须包含 {plan_id, operator, plan_name, monthly_price, data_allowance, validity_days, included_benefits[], excluded_benefits[], add_on_options[], fup_threshold, fup_action} 。Gemini在解析一份Reliance Jio PDF时,即使原文写的是“Up to 1.5GB/day till FUP of 100GB/month, after which speed reduced to 64kbps”,也能精准拆解出 "data_allowance": "1.5GB/day" , "fup_threshold": "100GB/month" , "fup_action": "speed_reduced_to_64kbps" 。我们做过压力测试:连续10万次调用,结构化失败率仅0.03%,且错误都集中在极少数OCR识别失败的PDF页(此时自动触发人工复核流程)。这种确定性,是生产环境的生命线——没有它,AI就只是个玩具。
2.3 “Production-ready”的三大硬性指标如何落地?
标题里那个括号不是装饰。所谓Production-ready,在电信系统里意味着三件事必须100%达标: 数据新鲜度≤2小时、单点故障零容忍、合规审计全留痕 。
- 数据新鲜度 :印度运营商平均每周上线3-5个新套餐,下架2-4个旧套餐。我们采用“双通道增量同步”:主通道对接运营商官方API(如Airtel Developer Portal、BSNL OpenData),每15分钟拉取变更;备用通道部署无头浏览器集群(Puppeteer Cluster),定时抓取官网套餐页,用Gemini做差异比对。一旦发现API未推送但网页已更新,立即告警并启动人工确认流程。实测端到端延迟稳定在1.8小时内。
- 高可用 :图数据库集群采用Neo4j Causal Cluster三节点部署(1主2从),所有写操作走Leader,读操作负载均衡到Followers。AI服务层用Kubernetes滚动更新,Pod副本数根据QPS自动伸缩(阈值设为>500 QPS时扩容)。最狠的是降级策略:当Gemini API临时不可用(概率约0.1%/天),系统自动切换至预训练的轻量级NER模型(DistilBERT微调版),虽准确率降5%,但保证基础字段(价格、有效期、数据量)100%可用,绝不返回“服务不可用”。
- 审计留痕 :每个套餐节点在Neo4j中都带
source_url、scraped_at、ai_parsed_by、human_verified_at(若有人工复核)等属性。所有Gemini调用请求/响应均加密落库,保留365天。某次BSNL调整FUP政策,我们30分钟内就定位到受影响的172个套餐,并自动生成合规检查报告——这能力,是靠堆砌技术指标换不来的。
3. 核心实现细节与实操要点
3.1 数据采集与清洗:从混乱PDF到标准图谱的炼金术
印度电信资料的混乱程度,超乎想象。我们爬取的首批12万份文档里,63%是扫描版PDF(OCR识别错误率高达18%),22%是HTML表格但列名随意(“Monthly Charge”、“Plan Cost (Rs.)”、“₹/Month”并存),15%是纯图片海报。传统ETL流程在这里完全失效。我们的解决方案是“三层过滤清洗流水线”:
第一层:文档类型智能分拣
用Python+pdfplumber快速检测PDF是否含可提取文本。若为扫描件,调用Google Cloud Vision API进行高精度OCR(特别针对印度手写体数字优化);若为文本型PDF,用正则匹配常见结构化模式(如“Price: ₹XXX”、“Data: Y GB/Month”)。对HTML,用BeautifulSoup提取所有 <table> ,再用 pandas.read_html() 转DataFrame。这一步将原始文档归类为“高可信文本”、“OCR待校验”、“图像海报”三类,分流处理。
第二层:Gemini驱动的语义清洗
这是最关键的一步。我们不直接让Gemini解析原始文本,而是先用规则引擎做初筛:提取所有疑似价格(带₹符号+数字)、疑似数据量(含GB/MB/TB字样)、疑似有效期(含days/months/year字样)的片段,拼成一段“候选字段摘要”。再把这个摘要喂给Gemini,prompt明确要求:“你是一个印度电信资费专家,请从以下摘要中,严格按JSON Schema提取字段。若某字段原文未提及,填null;若存在歧义(如‘unlimited’未说明是否含FUP),在 ambiguity_notes 字段中说明。” 例如摘要:“Jio Postpaid 299: ₹299/mo, 2GB/day, unlimited voice, FUP 100GB, then 64kbps”。Gemini输出:
{
"monthly_price": 299,
"data_allowance": "2GB/day",
"voice_allowance": "unlimited",
"fup_threshold": "100GB",
"fup_action": "speed_reduced_to_64kbps",
"ambiguity_notes": ["'unlimited voice'未说明是否含国际长途"]
}
这个设计大幅降低幻觉率——Gemini只在有限上下文中做判断,而非面对整页混乱文本自由发挥。
第三层:图谱化注入与冲突消解
清洗后的JSON进入Neo4j前,要解决“同款套餐不同名”问题。比如Airtel的“Digital TV + Broadband 399”和“Broadband 399 with Free Digital TV”实为同一套餐。我们用Sentence-BERT计算套餐描述向量相似度,阈值设为0.85。若相似度>0.85且价格/数据量一致,则合并为同一节点,保留所有来源URL。冲突时(如两家爬虫抓到不同FUP值),触发人工审核队列,标注“conflict_pending_verification”。这套流程使数据入库准确率从初期的71%提升至99.4%,日均处理文档量达8000+份。
3.2 图模型设计:如何让“套餐”真正活起来?
图数据库的价值,70%在模型设计。我们没用教科书式的“套餐-权益”二元模型,而是构建了五层语义网络:
-
Layer 1:核心实体层
Plan(套餐)、Operator(运营商)、Benefit(权益)、Policy(政策)、UserSegment(用户群)。每个节点带标准化属性,如Plan.price_currency="INR"、Benefit.category="data"。 -
Layer 2:动态关系层
关系类型不是静态的。Plan到Benefit有INCLUDES(标配)、OFFERS_AS_ADDON(可选包)、EXCLUDES(明确不含);Benefit到Policy有GOVERNED_BY(受某政策约束)、VIOLATES_IF(违反某条件则失效)。例如:Plan:JioFiber_Gold-[INCLUDES]->Benefit:Free_Landline-[GOVERNED_BY]->Policy:Domestic_Calling_Only。 -
Layer 3:时空维度层
所有关系带valid_from和valid_to属性。当运营商下架旧套餐,不是删除节点,而是将INCLUDES关系的valid_to设为下架日期。历史查询仍可追溯。 -
Layer 4:约束表达层
用Constraint节点表达复杂条件。如“学生套餐需提供.edu邮箱验证”,建模为:Plan:Student_Plan-[REQUIRES]->Constraint:Email_Verification-[HAS_PATTERN]->Pattern:".*@.*\.edu"。 -
Layer 5:影响传播层
当Policy:FUP_Change节点更新,自动触发Cypher脚本,标记所有GOVERNED_BY此政策的Benefit节点为needs_revalidation,通知下游系统重新计算套餐推荐。
这个模型让查询能力质变。比如销售顾问问:“帮我找适合大学生、含国际漫游、且FUP后不限速的套餐”,对应Cypher:
MATCH (p:Plan)-[:INCLUDES]->(b:Benefit {category:"international_roaming"})
WHERE (p)-[:APPLIES_TO]->(:UserSegment {name:"Student"})
AND NOT (b)-[:GOVERNED_BY]->(:Policy {name:"FUP_Speed_Reduction"})
RETURN p.name, p.monthly_price
无需任何应用层代码,图数据库原生支持。
3.3 Gemini结构化输出的工程化封装
让Gemini稳定输出JSON,远不止设置 response_mime_type 那么简单。我们踩过三个深坑:
坑一:Prompt注入攻击
早期用简单prompt:“请输出JSON”,结果某次解析Vodafone PDF时,Gemini在JSON末尾追加了“P.S. This is a demo response.”——下游系统JSON解析直接崩溃。解决方案:在prompt末尾强制添加分隔符,并在代码层做双重校验:
# Prompt结尾
"""Output ONLY valid JSON. Do not add any text before or after the JSON object. Use this exact delimiter after JSON: ###END_OF_JSON###"""
# 解析时
json_str = response.text.split("###END_OF_JSON###")[0].strip()
json.loads(json_str) # 若失败,记录原始response并告警
坑二:长文本截断导致Schema缺失
Gemini对输入长度敏感。一份含20页条款的PDF,若全量喂入,常因token超限丢失关键字段。对策是“分段聚焦解析”:先用轻量模型(spaCy)提取全文关键段落(价格页、FUP条款页、附加服务页),再将每段单独送入Gemini,最后用规则合并结果。实测准确率提升22%,且耗时减少35%。
坑三:空值与默认值混淆
运营商常写“Voice: Unlimited”,但未说明是否含国际长途。Gemini有时填 "voice_includes_intl": true (幻觉),有时填 null (安全但信息不足)。我们引入“置信度标注”:Gemini输出增加 confidence_score 字段(0.0-1.0),对 null 值且 confidence_score<0.3 的字段,自动标记为 needs_human_review 。目前92%的套餐可全自动入库,剩余8%进入人工队列,平均处理时长<90秒。
4. 实操全流程与关键配置详解
4.1 从零搭建环境:最小可行集群配置
别被“Production-ready”吓住。我们验证过的最小可行集群,仅需3台16GB内存、4核CPU的云服务器(AWS t3.xlarge或GCP e2-standard-4),成本可控:
- Server 1(AI Gateway) :部署FastAPI服务,封装Gemini调用。关键配置:
# config.py GEMINI_CONFIG = { "model": "gemini-1.5-pro", "temperature": 0.1, # 严控创造性,确保事实性 "max_output_tokens": 2048, "response_mime_type": "application/json" } # 设置重试:3次指数退避,超时30秒 - Server 2(Neo4j Core) :Neo4j 5.21社区版(企业版非必需)。
neo4j.conf关键调优:# 内存分配(16GB机器) dbms.memory.heap.initial_size=6g dbms.memory.heap.max_size=6g dbms.memory.pagecache.size=6g # 启用全文索引加速搜索 dbms.index.fulltext.enabled=true dbms.index.fulltext.analyser=indian_english # 安全:强制HTTPS,禁用未授权访问 dbms.connector.bolt.enabled=true dbms.connector.bolt.tls_level=REQUIRED - Server 3(Data Pipeline) :运行Airflow调度器+爬虫Worker。Docker Compose关键片段:
services: webserver: image: apache/airflow:2.8.1 environment: - AIRFLOW__CORE__EXECUTOR=CeleryExecutor - AIRFLOW__CELERY__RESULT_BACKEND=rpc:// scraper-worker: build: ./scraper environment: - SCRAPING_CONCURRENCY=10 # 每Worker并发10个页面
这套配置支撑日均50万次查询、2万次套餐更新,CPU平均负载<45%。升级路径清晰:查询压力大时,加Neo4j Read Replica;AI调用量增,水平扩展FastAPI Pod。
4.2 套餐入库全流程:一次典型任务的7个步骤
以解析一份Airtel 4G Postpaid PDF为例,完整流程如下:
- 触发 :Airflow DAG检测到
airtel_postpaid_2024_q3.pdf上传至S3,启动parse_plan_pdf任务。 - 文档解析 :调用pdfplumber,若检测为扫描件,转交Google Vision OCR,获取文本块坐标。
- 关键段落提取 :用正则匹配
r"Price.*?₹\d+"定位价格区,r"FUP.*?\d+GB"定位FUP条款,裁剪出3个文本块。 - Gemini调用 :将3个文本块分别构造prompt,发送至FastAPI
/gemini/parse端点。收到3个JSON响应。 - 数据融合 :Python脚本合并3个JSON,冲突字段(如两个文本块给出不同有效期)标记
conflict_pending。 - 图谱写入 :生成Cypher语句:
MERGE (p:Plan {plan_id:"AIRTEL_POSTPAID_299"}) ON CREATE SET p.operator="Airtel", p.plan_name="Postpaid 299", p.monthly_price=299 WITH p MERGE (b:Benefit {name:"2GB/day Data"}) MERGE (p)-[r:INCLUDES]->(b) SET r.valid_from=date("2024-07-01") - 验证与发布 :执行
MATCH (p:Plan {plan_id:"AIRTEL_POSTPAID_299"}) RETURN count(*)确认节点存在,触发notify_downstream_systems任务,向客服机器人API推送更新事件。
整个流程平均耗时8.2秒,99.9%任务在15秒内完成。失败任务自动重试2次,仍失败则转入人工队列。
4.3 面向业务的查询接口设计
图数据库的强大,最终要落到业务能用。我们暴露了三类REST API:
- 基础检索 :
GET /plans?operator=Jio&min_data=1GB&includes=international_roaming
转换为Cypher:MATCH (p:Plan) WHERE p.operator CONTAINS "Jio" AND ... RETURN p - 关系推理 :
POST /plans/recommend,Body传用户画像:
后端生成Cypher:{ "user_segment": "Student", "current_usage": {"data_gb": 85, "intl_minutes": 120}, "constraints": ["no_contract", "fup_no_speed_drop"] }MATCH (p:Plan)-[:APPLIES_TO]->(:UserSegment {name:"Student"}) ... WHERE NOT (p)-[:GOVERNED_BY]->(:Policy {name:"Contract_Mandatory"}) ... RETURN p ORDER BY p.monthly_price LIMIT 5 - 影响分析 :
GET /policies/FUP_Change_2024/impact
返回所有受此政策变更影响的套餐列表及具体权益变动。
所有API均集成OpenAPI 3.0规范,自动生成Swagger文档,前端团队1小时即可接入。某次Airtel调整FUP,客服系统3分钟内就收到影响清单,主动外呼高风险用户——这种响应速度,是传统数据库做不到的。
5. 常见问题与实战排障指南
5.1 典型问题速查表
| 问题现象 | 根本原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| Gemini返回非JSON文本 | Prompt被PDF中的特殊字符(如页眉页脚乱码)污染 | 1. 查看原始PDF OCR文本;2. 检查prompt中是否混入不可见Unicode字符 | 在文本预处理阶段,用 re.sub(r'[^\x20-\x7E]', ' ', text) 清理非ASCII字符 |
| Neo4j查询超时(>30s) | 复杂路径查询未建索引,或关系数量爆炸 | 1. EXPLAIN 查询计划;2. 检查 NodeByLabelScan 是否出现 |
对高频查询字段建索引: CREATE INDEX plan_operator_name ON :Plan(operator, plan_name) |
| 同一套餐重复入库 | 爬虫未去重,或URL参数不同(如 ?ref=fb vs ?ref=google ) |
1. 查询 MATCH (p:Plan) WHERE p.source_url CONTAINS "airtel" ;2. 统计 p.plan_id 分布 |
在爬虫层,用 urllib.parse.urlparse(url).path 提取纯净路径,忽略query参数 |
| FUP政策变更未触发影响分析 | Policy 节点更新,但 GOVERNED_BY 关系未带 valid_to 属性 |
1. MATCH (p:Policy {name:"FUP_Change"}) RETURN p ;2. 检查 p.valid_to 是否存在 |
强制所有 Policy 节点必填 valid_from , valid_to 为空表示永久有效 |
| 学生套餐推荐遗漏 | UserSegment 节点未与 Plan 正确关联 |
1. MATCH (s:UserSegment {name:"Student"}) RETURN count(*) ;2. MATCH (p:Plan)-[r:APPLIES_TO]->(s) RETURN count(r) |
建立数据质量监控:每日凌晨运行 MATCH (s:UserSegment) WHERE NOT (s)<-[:APPLIES_TO]-() RETURN s.name ,告警未关联的用户群 |
5.2 我踩过的三个血泪坑与独家技巧
坑一:PDF字体嵌入导致OCR失败
某次解析BSNL PDF,Vision API返回空文本。抓包发现PDF内嵌了自定义字体(BSNL_logo_font),OCR引擎无法识别。常规方案是PDF转图片再OCR,但质量损失大。我的解法:用 pdf2image 将PDF转PNG时,强制指定DPI=300,并添加 --use-cropbox 参数保留原始裁剪框,再喂给Vision API。效果提升显著,错误率从41%降至6%。
坑二:Gemini对“unlimited”的语义漂移
Gemini有时把“unlimited data”解析为 data_allowance: "unlimited" ,有时为 data_allowance: "unlimited (FUP 100GB)" 。根源是训练数据中“unlimited”一词的歧义性。我的技巧:在prompt中明确定义——“若原文未提FUP,一律填 "unlimited" ;若提到FUP,填 "unlimited (FUP X)" ;若只写‘Fair Usage Policy applies’但未给数值,填 "unlimited (FUP_not_specified)" 。用括号格式强制统一,下游系统解析时只需 re.search(r'FUP\s+(\d+\w+)', value) 即可提取数值。
坑三:Neo4j关系数量爆炸
初期设计 Plan 到 Benefit 用 INCLUDES 一种关系,结果一个“全家桶套餐”连出27个 INCLUDES 关系,图遍历性能骤降。重构后,引入 BenefitGroup 节点: Plan -[INCLUDES_GROUP]-> BenefitGroup -[CONTAINS]-> Benefit 。一个套餐最多连3-5个Group(如“Data Pack”、“Voice Pack”、“Entertainment Pack”),再由Group展开具体权益。查询性能提升4倍,且业务语义更清晰——销售时可说“这个套餐的数据包很厚”,而非罗列27项。
最后分享个小技巧: 永远用“业务语言”命名节点和关系 。不要叫 NodeA 、 Rel1 ,而要用 Plan 、 INCLUDES 、 UserSegment 。我见过太多团队因命名随意,半年后连自己都看不懂Cypher查询在干什么。图数据库的威力,一半在模型,一半在可读性——毕竟,最终维护它的是人,不是机器。
更多推荐


所有评论(0)