argo-search 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,689 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ clarify.py — 意图消歧工具(wigolo query_understanding 理念移植)
4
+
5
+ 核心能力:
6
+ 1. 歧义检测:识别查询中的多义词
7
+ 2. 意图分类:判断查询的真实意图
8
+ 3. 推荐路由:给出最优搜索策略
9
+ 4. 多义展开:列出所有可能含义供用户选择
10
+
11
+ 用法:
12
+ python3 clarify.py "Python 吞苹果 兼容吗"
13
+ python3 clarify.py "苹果股价" --explain
14
+ python3 clarify.py "Java" --json
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import json
21
+ import re
22
+ import sys
23
+ from typing import Any
24
+
25
+
26
+ # ── 歧义词库 ──────────────────────────────────────────────────────────────────
27
+
28
+ AMBIGUOUS_TERMS = {
29
+ # 多义实体
30
+ "苹果": {
31
+ "meanings": [
32
+ {"text": "Apple 公司(科技/股票)", "domain": "tech", "weight": 0.6},
33
+ {"text": "苹果(水果/食品)", "domain": "general", "weight": 0.3},
34
+ {"text": "苹果(操作系统/macOS)", "domain": "tech", "weight": 0.1},
35
+ ],
36
+ "disambiguation_keywords": {
37
+ "tech": ["股价", "股票", "iPhone", "Mac", "iOS", "WWDC", "市值", "AAPL", "库克", "蒂姆"],
38
+ "food": ["吃", "水果", "营养", "减肥", "种植", "产地", "品种"],
39
+ },
40
+ },
41
+ "Python": {
42
+ "meanings": [
43
+ {"text": "Python 编程语言", "domain": "tech", "weight": 0.7},
44
+ {"text": "蟒蛇(动物)", "domain": "nature", "weight": 0.2},
45
+ {"text": "Monty Python(喜剧团体)", "domain": "entertainment", "weight": 0.1},
46
+ ],
47
+ "disambiguation_keywords": {
48
+ "tech": ["代码", "编程", "函数", "库", "框架", "pip", "安装", "版本", "async", "Django", "Flask", "报错", "bug"],
49
+ "nature": ["蛇", "爬", "动物", "宠物", "饲养", "鳞片"],
50
+ "entertainment": ["电影", "喜剧", "英国", "动画"],
51
+ },
52
+ },
53
+ "Java": {
54
+ "meanings": [
55
+ {"text": "Java 编程语言", "domain": "tech", "weight": 0.7},
56
+ {"text": "印度尼西亚爪哇岛", "domain": "travel", "weight": 0.15},
57
+ {"text": "Java 咖啡", "domain": "food", "weight": 0.15},
58
+ ],
59
+ "disambiguation_keywords": {
60
+ "tech": ["编程", "代码", "JDK", "Spring", "Maven", "Gradle", "JVM", "报错", "版本"],
61
+ "travel": ["旅游", "岛", "签证", "机票", "酒店"],
62
+ "food": ["咖啡", "豆", "烘焙", "产地"],
63
+ },
64
+ },
65
+ "Rust": {
66
+ "meanings": [
67
+ {"text": "Rust 编程语言", "domain": "tech", "weight": 0.7},
68
+ {"text": "锈蚀(化学/材料)", "domain": "science", "weight": 0.2},
69
+ {"text": "Rust 游戏", "domain": "gaming", "weight": 0.1},
70
+ ],
71
+ "disambiguation_keywords": {
72
+ "tech": ["编程", "cargo", "crate", "所有权", "borrow", "编译", "代码", "性能"],
73
+ "science": ["金属", "腐蚀", "防护", "涂层", "氧化"],
74
+ "gaming": ["游戏", "服务器", "联机", "steam"],
75
+ },
76
+ },
77
+ "茅台": {
78
+ "meanings": [
79
+ {"text": "贵州茅台(股票/白酒品牌)", "domain": "finance", "weight": 0.7},
80
+ {"text": "茅台镇(地名/产区)", "domain": "travel", "weight": 0.2},
81
+ {"text": "茅台酒(产品/品鉴)", "domain": "lifestyle", "weight": 0.1},
82
+ ],
83
+ "disambiguation_keywords": {
84
+ "finance": ["股价", "股票", "市值", "财报", "营收", "利润", "分红", "600519"],
85
+ "travel": ["镇", "旅游", "产区", "参观", "路线"],
86
+ "lifestyle": ["口感", "品鉴", "收藏", "年份", "真假"],
87
+ },
88
+ },
89
+ "Transformer": {
90
+ "meanings": [
91
+ {"text": "Transformer 模型(AI/深度学习)", "domain": "tech", "weight": 0.6},
92
+ {"text": "变形金刚(玩具/电影)", "domain": "entertainment", "weight": 0.25},
93
+ {"text": "变压器(电气设备)", "domain": "engineering", "weight": 0.15},
94
+ ],
95
+ "disambiguation_keywords": {
96
+ "tech": ["attention", "BERT", "GPT", "神经网络", "NLP", "论文", "模型", "训练", "AI", "大模型"],
97
+ "entertainment": ["电影", "玩具", "擎天柱", "大黄蜂", "动画"],
98
+ "engineering": ["电力", "电压", "电网", "变电站"],
99
+ },
100
+ },
101
+ # ── 扩展歧义词库(v2.1 新增 30+ 词)──
102
+ "小米": {
103
+ "meanings": [
104
+ {"text": "小米科技(手机/IoT品牌)", "domain": "tech", "weight": 0.6},
105
+ {"text": "小米(谷物/粮食)", "domain": "food", "weight": 0.3},
106
+ {"text": "小米SU7(汽车)", "domain": "auto", "weight": 0.1},
107
+ ],
108
+ "disambiguation_keywords": {
109
+ "tech": ["手机", "MIUI", "HyperOS", "雷军", "SU7", "市值", "港股", "1810"],
110
+ "food": ["粮食", "作物", "种植", "产量", "小米粥"],
111
+ },
112
+ },
113
+ "华为": {
114
+ "meanings": [
115
+ {"text": "华为技术有限公司", "domain": "tech", "weight": 0.8},
116
+ {"text": "华为(人名/地名)", "domain": "general", "weight": 0.2},
117
+ ],
118
+ "disambiguation_keywords": {"tech": ["手机", "鸿蒙", "5G", "芯片", "Mate", "任正非"]},
119
+ },
120
+ "特斯拉": {
121
+ "meanings": [
122
+ {"text": "Tesla(电动汽车品牌)", "domain": "auto", "weight": 0.8},
123
+ {"text": "尼古拉·特斯拉(发明家)", "domain": "science", "weight": 0.2},
124
+ ],
125
+ "disambiguation_keywords": {
126
+ "auto": ["Model", "充电桩", "自动驾驶", "FSD", "马斯克"],
127
+ "science": ["交流电", "发明", "无线电", "特斯拉线圈"],
128
+ },
129
+ },
130
+ "Docker": {
131
+ "meanings": [
132
+ {"text": "Docker 容器化平台", "domain": "tech", "weight": 0.85},
133
+ {"text": "码头工人", "domain": "general", "weight": 0.15},
134
+ ],
135
+ "disambiguation_keywords": {"tech": ["容器", "镜像", "compose", "Kubernetes", "部署"]},
136
+ },
137
+ "Go": {
138
+ "meanings": [
139
+ {"text": "Go 编程语言", "domain": "tech", "weight": 0.7},
140
+ {"text": "去/进行(动词)", "domain": "general", "weight": 0.3},
141
+ ],
142
+ "disambiguation_keywords": {"tech": ["Golang", "并发", "goroutine", "channel", "编译", "stdlib"]},
143
+ },
144
+ "Redis": {
145
+ "meanings": [
146
+ {"text": "Redis 缓存数据库", "domain": "tech", "weight": 0.9},
147
+ {"text": "Redis(人名)", "domain": "general", "weight": 0.1},
148
+ ],
149
+ "disambiguation_keywords": {"tech": ["缓存", "内存", "键值", "持久化", "集群"]},
150
+ },
151
+ "Swift": {
152
+ "meanings": [
153
+ {"text": "Swift 编程语言(Apple)", "domain": "tech", "weight": 0.7},
154
+ {"text": "Swift(金融转账系统)", "domain": "finance", "weight": 0.15},
155
+ {"text": "Swift(歌手)", "domain": "entertainment", "weight": 0.15},
156
+ ],
157
+ "disambiguation_keywords": {
158
+ "tech": ["iOS", "macOS", "Xcode", "SwiftUI", "苹果"],
159
+ "finance": ["转账", "银行", "国际结算"],
160
+ },
161
+ },
162
+ "Kotlin": {
163
+ "meanings": [
164
+ {"text": "Kotlin 编程语言", "domain": "tech", "weight": 0.85},
165
+ {"text": "Kotlin(地名/岛名)", "domain": "travel", "weight": 0.15},
166
+ ],
167
+ "disambiguation_keywords": {"tech": ["Android", "JVM", "JetBrains", "协程", "空安全"]},
168
+ },
169
+ "抖音": {
170
+ "meanings": [
171
+ {"text": "抖音/TikTok(短视频平台)", "domain": "tech", "weight": 0.8},
172
+ {"text": "抖音(动词/拟声)", "domain": "general", "weight": 0.2},
173
+ ],
174
+ "disambiguation_keywords": {"tech": ["短视频", "直播", "字节跳动", "TikTok", "算法"]},
175
+ },
176
+ "微信": {
177
+ "meanings": [
178
+ {"text": "微信(通讯/社交平台)", "domain": "tech", "weight": 0.85},
179
+ {"text": "微信(动词)", "domain": "general", "weight": 0.15},
180
+ ],
181
+ "disambiguation_keywords": {"tech": ["小程序", "公众号", "支付", "腾讯", "朋友圈"]},
182
+ },
183
+ "GPT": {
184
+ "meanings": [
185
+ {"text": "GPT 系列大语言模型(OpenAI)", "domain": "tech", "weight": 0.8},
186
+ {"text": "GPT(GUID Partition Table)", "domain": "tech", "weight": 0.2},
187
+ ],
188
+ "disambiguation_keywords": {"tech": ["OpenAI", "ChatGPT", "大模型", "GPT-4", "GPT-5"]},
189
+ },
190
+ "RAG": {
191
+ "meanings": [
192
+ {"text": "RAG(检索增强生成)", "domain": "tech", "weight": 0.7},
193
+ {"text": "RAG(摇滚乐风格)", "domain": "music", "weight": 0.15},
194
+ {"text": "Rag(抹布)", "domain": "general", "weight": 0.15},
195
+ ],
196
+ "disambiguation_keywords": {"tech": ["检索", "向量", "embedding", "大模型", "知识库"]},
197
+ },
198
+ "芯片": {
199
+ "meanings": [
200
+ {"text": "半导体芯片(IC)", "domain": "tech", "weight": 0.7},
201
+ {"text": "薯片/食品", "domain": "food", "weight": 0.2},
202
+ {"text": "筹码(赌博)", "domain": "general", "weight": 0.1},
203
+ ],
204
+ "disambiguation_keywords": {
205
+ "tech": ["半导体", "NVIDIA", "台积电", "ASML", "光刻机", "GPU"],
206
+ "food": ["零食", "好吃", "口味", "乐事"],
207
+ },
208
+ },
209
+ "蔚来": {
210
+ "meanings": [
211
+ {"text": "蔚来汽车(NIO)", "domain": "auto", "weight": 0.8},
212
+ {"text": "蔚蓝/蔚然(形容词)", "domain": "general", "weight": 0.2},
213
+ ],
214
+ "disambiguation_keywords": {"auto": ["电动车", "换电", "NIO", "李斌", "ET5", "ES6"]},
215
+ },
216
+ "理想": {
217
+ "meanings": [
218
+ {"text": "理想汽车(Li Auto)", "domain": "auto", "weight": 0.6},
219
+ {"text": "理想(概念/哲学)", "domain": "general", "weight": 0.4},
220
+ ],
221
+ "disambiguation_keywords": {"auto": ["增程", "L系列", "MEGA", "李想", "纯电"]},
222
+ },
223
+ "小鹏": {
224
+ "meanings": [
225
+ {"text": "小鹏汽车(XPeng)", "domain": "auto", "weight": 0.8},
226
+ {"text": "小鹏(人名)", "domain": "general", "weight": 0.2},
227
+ ],
228
+ "disambiguation_keywords": {"auto": ["P7", "G6", "飞行汽车", "何小鹏", "智驾"]},
229
+ },
230
+ "Anthropic": {
231
+ "meanings": [
232
+ {"text": "Anthropic(AI 公司/Claude)", "domain": "tech", "weight": 0.85},
233
+ {"text": "Anthropic(人类学的)", "domain": "science", "weight": 0.15},
234
+ ],
235
+ "disambiguation_keywords": {"tech": ["Claude", "大模型", "AI安全", "Constitutional AI"]},
236
+ },
237
+ "OpenAI": {
238
+ "meanings": [
239
+ {"text": "OpenAI(AI 公司)", "domain": "tech", "weight": 0.9},
240
+ {"text": "OpenAI(开源 AI 组织)", "domain": "tech", "weight": 0.1},
241
+ ],
242
+ "disambiguation_keywords": {"tech": ["GPT", "ChatGPT", "Sam Altman", "DALL-E", "Sora"]},
243
+ },
244
+ "Cursor": {
245
+ "meanings": [
246
+ {"text": "Cursor(AI 代码编辑器)", "domain": "tech", "weight": 0.7},
247
+ {"text": "光标/鼠标指针", "domain": "general", "weight": 0.3},
248
+ ],
249
+ "disambiguation_keywords": {"tech": ["编辑器", "VSCode", "AI编程", "Tab补全", "Composer"]},
250
+ },
251
+ "Gemini": {
252
+ "meanings": [
253
+ {"text": "Google Gemini(AI 模型)", "domain": "tech", "weight": 0.7},
254
+ {"text": "双子座(星座)", "domain": "general", "weight": 0.2},
255
+ {"text": "Gemini(电影工作室)", "domain": "entertainment", "weight": 0.1},
256
+ ],
257
+ "disambiguation_keywords": {"tech": ["Google", "大模型", "Gemini Pro", "多模态"]},
258
+ },
259
+ "Claude": {
260
+ "meanings": [
261
+ {"text": "Claude(Anthropic AI 助手)", "domain": "tech", "weight": 0.8},
262
+ {"text": "Claude(法语人名)", "domain": "general", "weight": 0.2},
263
+ ],
264
+ "disambiguation_keywords": {"tech": ["Anthropic", "大模型", "Claude 3", "AI助手"]},
265
+ },
266
+ "Notion": {
267
+ "meanings": [
268
+ {"text": "Notion(协作/笔记工具)", "domain": "tech", "weight": 0.85},
269
+ {"text": "概念/观念", "domain": "general", "weight": 0.15},
270
+ ],
271
+ "disambiguation_keywords": {"tech": ["笔记", "协作", "数据库", "模板", "AI"]},
272
+ },
273
+ "飞书": {
274
+ "meanings": [
275
+ {"text": "飞书(字节跳动协作工具)", "domain": "tech", "weight": 0.8},
276
+ {"text": "飞书(飞来的书信)", "domain": "general", "weight": 0.2},
277
+ ],
278
+ "disambiguation_keywords": {"tech": ["协作", "OKR", "视频会议", "文档", "多维表格"]},
279
+ },
280
+ "比特币": {
281
+ "meanings": [
282
+ {"text": "Bitcoin(加密货币)", "domain": "finance", "weight": 0.85},
283
+ {"text": "比特币(泛指加密货币)", "domain": "finance", "weight": 0.15},
284
+ ],
285
+ "disambiguation_keywords": {"finance": ["BTC", "挖矿", "区块链", "交易所", "减半", "ETF"]},
286
+ },
287
+ "新能源": {
288
+ "meanings": [
289
+ {"text": "新能源汽车", "domain": "auto", "weight": 0.6},
290
+ {"text": "新能源(太阳能/风能等)", "domain": "energy", "weight": 0.4},
291
+ ],
292
+ "disambiguation_keywords": {
293
+ "auto": ["电动车", "比亚迪", "特斯拉", "充电桩", "续航"],
294
+ "energy": ["光伏", "风电", "储能", "碳中和"],
295
+ },
296
+ },
297
+ "A股": {
298
+ "meanings": [
299
+ {"text": "A股市场(中国股票)", "domain": "finance", "weight": 0.9},
300
+ {"text": "A股(游戏术语)", "domain": "gaming", "weight": 0.1},
301
+ ],
302
+ "disambiguation_keywords": {"finance": ["上证", "深证", "创业板", "科创板", "北交所"]},
303
+ },
304
+ "ETF": {
305
+ "meanings": [
306
+ {"text": "ETF(交易所交易基金)", "domain": "finance", "weight": 0.9},
307
+ {"text": "ETF(其他缩写)", "domain": "general", "weight": 0.1},
308
+ ],
309
+ "disambiguation_keywords": {"finance": ["指数基金", "净值", "场内", "LOF", "QDII"]},
310
+ },
311
+ "量化": {
312
+ "meanings": [
313
+ {"text": "量化投资/量化交易", "domain": "finance", "weight": 0.7},
314
+ {"text": "量化(物理学/化学)", "domain": "science", "weight": 0.3},
315
+ ],
316
+ "disambiguation_keywords": {
317
+ "finance": ["对冲", "因子", "Alpha", "回测", "高频", "策略"],
318
+ "science": ["测量", "分析", "浓度", "实验"],
319
+ },
320
+ },
321
+ "期权": {
322
+ "meanings": [
323
+ {"text": "期权(金融衍生品)", "domain": "finance", "weight": 0.85},
324
+ {"text": "期权(选择权/选项)", "domain": "general", "weight": 0.15},
325
+ ],
326
+ "disambiguation_keywords": {"finance": ["认购", "认沽", "行权", "波动率", "Greeks"]},
327
+ },
328
+ "基金": {
329
+ "meanings": [
330
+ {"text": "公募/私募基金", "domain": "finance", "weight": 0.8},
331
+ {"text": "基金(基础/根本)", "domain": "general", "weight": 0.2},
332
+ ],
333
+ "disambiguation_keywords": {"finance": ["净值", "回撤", "夏普", "基金经理", "ETF"]},
334
+ },
335
+ }
336
+
337
+ # ── 品牌碰撞检测 ──────────────────────────────────────────────────────────────
338
+
339
+ BRAND_COLLISIONS = {
340
+ "Amazon": {"domains": ["amazon.com", "aws.amazon.com"], "alt": ["亚马逊(河流/地区)"]},
341
+ "Apple": {"domains": ["apple.com"], "alt": ["苹果(水果)"]},
342
+ "小米": {"domains": ["xiaomi.com", "mi.com"], "alt": ["小米(谷物)"]},
343
+ "华为": {"domains": ["huawei.com"], "alt": ["华为(人名)"]},
344
+ "特斯拉": {"domains": ["tesla.com"], "alt": ["尼古拉·特斯拉(发明家)"]},
345
+ "字节": {"domains": ["bytedance.com"], "alt": ["字节(计算机单位)"]},
346
+ "快手": {"domains": ["kuaishou.com"], "alt": ["快手(手快)"]},
347
+ "贝壳": {"domains": ["ke.com", "beike.com"], "alt": ["贝壳(海洋生物)"]},
348
+ "飞书": {"domains": ["feishu.cn"], "alt": ["飞来的书信"]},
349
+ "钉钉": {"domains": ["dingtalk.com"], "alt": ["钉钉子的声音"]},
350
+ "美团": {"domains": ["meituan.com"], "alt": ["美好的团体"]},
351
+ "京东": {"domains": ["jd.com"], "alt": ["京东(地名)"]},
352
+ "淘宝": {"domains": ["taobao.com"], "alt": ["淘到的宝贝"]},
353
+ "拼多多": {"domains": ["pinduoduo.com"], "alt": ["拼凑"]},
354
+ "蔚来": {"domains": ["nio.com"], "alt": ["蔚蓝/蔚然"]},
355
+ "理想": {"domains": ["lixiang.com"], "alt": ["理想(概念)"]},
356
+ "小鹏": {"domains": ["xiaopeng.com"], "alt": ["小鹏(人名)"]},
357
+ "豆瓣": {"domains": ["douban.com"], "alt": ["豆瓣酱"]},
358
+ "雪球": {"domains": ["xueqiu.com"], "alt": ["雪球(游戏)"]},
359
+ "知乎": {"domains": ["zhihu.com"], "alt": ["知乎(文言文)"]},
360
+ "闲鱼": {"domains": ["goofish.com"], "alt": ["闲置的鱼"]},
361
+ "盒马": {"domains": ["hema.com"], "alt": ["盒马(动物)"]},
362
+ }
363
+
364
+
365
+ def detect_brand_collision(query: str) -> dict | None:
366
+ """检测查询中的品牌碰撞风险。"""
367
+ for brand, info in BRAND_COLLISIONS.items():
368
+ if brand in query or brand.lower() in query.lower():
369
+ return {
370
+ "brand": brand,
371
+ "collision_domains": info["domains"],
372
+ "alt_meanings": info["alt"],
373
+ "warning": f"「{brand}」可能指向多个品牌/产品",
374
+ }
375
+ return None
376
+
377
+
378
+ # 意图模式
379
+ INTENT_PATTERNS = {
380
+ "search_fact": {
381
+ "patterns": [
382
+ r"(?:多少|几|价格|值|身高|重量|面积|人口|GDP|增长率)",
383
+ r"(?:what|how many|how much|when|where|who)",
384
+ r"(?:是谁|是什么|在哪里|什么时候|多少钱)",
385
+ ],
386
+ "label": "事实查询",
387
+ "weight": 1.0,
388
+ },
389
+ "search_opinion": {
390
+ "patterns": [
391
+ r"(?:怎么看|如何评价|观点|想法|建议|推荐|意见)",
392
+ r"(?:how|why|what do you think|opinion|view)",
393
+ r"(?:值得|好不好|怎么样|该不该|有没有必要)",
394
+ ],
395
+ "label": "观点/评价",
396
+ "weight": 1.0,
397
+ },
398
+ "search_tech": {
399
+ "patterns": [
400
+ r"(?:怎么用|如何配置|安装|部署|报错|bug|error|教程|入门)",
401
+ r"(?:how to|tutorial|guide|setup|install|configure|troubleshoot)",
402
+ r"(?:API|SDK|框架|库|组件|插件|扩展)",
403
+ ],
404
+ "label": "技术操作",
405
+ "weight": 1.0,
406
+ },
407
+ "search_compare": {
408
+ "patterns": [
409
+ r"(?:vs| versus |对比|比较|区别|差异|选择|哪个好|优缺点)",
410
+ r"(?:A vs B|A or B|A compared to B)",
411
+ ],
412
+ "label": "对比分析",
413
+ "weight": 1.0,
414
+ },
415
+ "search_news": {
416
+ "patterns": [
417
+ r"(?:最新|今天|最近|新闻|动态|进展|发生|事件)",
418
+ r"(?:latest|recent|news|update|breaking)",
419
+ ],
420
+ "label": "新闻/动态",
421
+ "weight": 1.0,
422
+ },
423
+ "search_deep": {
424
+ "patterns": [
425
+ r"(?:深度|全面|详细|系统|完整|综述|研究|分析|探讨)",
426
+ r"(?:deep|comprehensive|review|survey|research|analysis)",
427
+ ],
428
+ "label": "深度研究",
429
+ "weight": 1.0,
430
+ },
431
+ }
432
+
433
+
434
+ # ── 核心分析 ──────────────────────────────────────────────────────────────────
435
+
436
+ def analyze_query(query: str) -> dict[str, Any]:
437
+ """分析查询的意图和歧义。"""
438
+ analysis = {
439
+ "query": query,
440
+ "language": _detect_language(query),
441
+ "ambiguities": [],
442
+ "intents": [],
443
+ "entities": [],
444
+ "recommended_strategy": "general",
445
+ "confidence": 0.8,
446
+ }
447
+
448
+ # 歧义检测
449
+ for term, info in AMBIGUOUS_TERMS.items():
450
+ if term in query or (term.isascii() and re.search(r'\b' + re.escape(term) + r'\b', query, re.I)):
451
+ # 检查上下文关键词
452
+ matched_meanings = []
453
+ for meaning in info["meanings"]:
454
+ domain = meaning["domain"]
455
+ keywords = info.get("disambiguation_keywords", {}).get(domain, [])
456
+ match_count = sum(1 for kw in keywords if kw in query)
457
+ # 强信号加分:产品型号、金融术语、学术术语
458
+ strong_bonus = 0
459
+ if domain == "auto" and re.search(r'(SU7|Model|ET5|ES6|P7|G6|L系列|MEGA|AION)', query, re.I):
460
+ strong_bonus = 0.3
461
+ elif domain == "finance" and re.search(r'(股价|财报|市值|基金|ETF|净值|涨跌|K线|分红)', query):
462
+ strong_bonus = 0.2
463
+ elif domain == "tech" and re.search(r'(论文|paper|arxiv|API|SDK|框架|库|编程|代码|编译|部署)', query, re.I):
464
+ strong_bonus = 0.15
465
+ elif domain == "energy" and re.search(r'(光伏|风电|储能|碳中和|太阳能|风能)', query):
466
+ strong_bonus = 0.2
467
+ if match_count > 0 or strong_bonus > 0:
468
+ matched_meanings.append({
469
+ "meaning": meaning["text"],
470
+ "domain": domain,
471
+ "context_match": match_count,
472
+ "base_weight": meaning["weight"],
473
+ "weight": meaning["weight"] + match_count * 0.1 + strong_bonus,
474
+ })
475
+
476
+ if matched_meanings:
477
+ matched_meanings.sort(key=lambda x: x["weight"], reverse=True)
478
+ top = matched_meanings[0]
479
+ # 置信度计算:基础分 + 关键词匹配加分 + 强信号加分
480
+ base_conf = 0.5
481
+ keyword_bonus = min(top["context_match"] * 0.12, 0.3)
482
+ # strong_bonus 从 weight 中提取(weight = base_weight + keyword*0.1 + strong_bonus)
483
+ strong_bonus = max(0, top["weight"] - top.get("base_weight", 0.5) - top["context_match"] * 0.1)
484
+ conf = min(base_conf + keyword_bonus + strong_bonus * 0.5, 0.95)
485
+ analysis["ambiguities"].append({
486
+ "term": term,
487
+ "possible_meanings": [m["meaning"] for m in matched_meanings],
488
+ "top_choice": top["meaning"],
489
+ "confidence": round(conf, 2),
490
+ })
491
+ analysis["confidence"] = min(analysis["confidence"], conf)
492
+ else:
493
+ # 无法消歧
494
+ analysis["ambiguities"].append({
495
+ "term": term,
496
+ "possible_meanings": [m["text"] for m in info["meanings"]],
497
+ "top_choice": info["meanings"][0]["text"],
498
+ "confidence": info["meanings"][0]["weight"],
499
+ })
500
+ analysis["confidence"] = min(analysis["confidence"], 0.6)
501
+
502
+ # 意图分类
503
+ for intent_key, intent_info in INTENT_PATTERNS.items():
504
+ for pattern in intent_info["patterns"]:
505
+ if re.search(pattern, query, re.I):
506
+ analysis["intents"].append({
507
+ "type": intent_key,
508
+ "label": intent_info["label"],
509
+ })
510
+ break
511
+
512
+ # 实体提取(简单规则)
513
+ # 英文专有名词
514
+ eng_entities = re.findall(r"\b([A-Z][a-zA-Z]+(?:\s+[A-Z][a-zA-Z]+)*)\b", query)
515
+ for e in eng_entities:
516
+ if len(e) > 2:
517
+ analysis["entities"].append({"text": e, "type": "proper_noun"})
518
+
519
+ # 中文专业术语
520
+ cn_tech = re.findall(r"([\u4e00-\u9fff]{2,4}(?:引擎|框架|工具|平台|模型|算法|协议|接口|数据库))", query)
521
+ for e in cn_tech:
522
+ analysis["entities"].append({"text": e, "type": "tech_term"})
523
+
524
+ # 编号/代码
525
+ codes = re.findall(r"\b(\d{6})\b", query)
526
+ for c in codes:
527
+ analysis["entities"].append({"text": c, "type": "stock_code"})
528
+
529
+ cve_codes = re.findall(r"(CVE-\d{4}-\d+)", query, re.I)
530
+ for c in cve_codes:
531
+ analysis["entities"].append({"text": c, "type": "cve"})
532
+
533
+ # 品牌碰撞检测
534
+ collision = detect_brand_collision(query)
535
+ if collision:
536
+ analysis["brand_collision"] = collision
537
+ analysis["confidence"] *= 0.85
538
+
539
+ # 推荐策略
540
+ if analysis["ambiguities"] and analysis["ambiguities"][0]["confidence"] < 0.7:
541
+ analysis["recommended_strategy"] = "clarify_first"
542
+ analysis["confidence"] = 0.5
543
+ elif any(i["type"] == "search_deep" for i in analysis["intents"]):
544
+ analysis["recommended_strategy"] = "deep_research"
545
+ elif any(i["type"] == "search_compare" for i in analysis["intents"]):
546
+ analysis["recommended_strategy"] = "split_search"
547
+ elif any(i["type"] == "search_fact" for i in analysis["intents"]):
548
+ analysis["recommended_strategy"] = "direct_search"
549
+ elif any(i["type"] == "search_news" for i in analysis["intents"]):
550
+ analysis["recommended_strategy"] = "news_priority"
551
+ else:
552
+ analysis["recommended_strategy"] = "general"
553
+
554
+ return analysis
555
+
556
+
557
+ def _detect_language(text: str) -> str:
558
+ """检测主要语言。"""
559
+ cn_count = sum(1 for c in text if "\u4e00" <= c <= "\u9fff")
560
+ en_count = sum(1 for c in text if c.isascii() and c.isalpha())
561
+ ja_count = sum(1 for c in text if "\u3040" <= c <= "\u30ff")
562
+
563
+ total = cn_count + en_count + ja_count
564
+ if total == 0:
565
+ return "unknown"
566
+
567
+ if cn_count / total > 0.5:
568
+ return "zh" if en_count / total < 0.3 else "zh-en"
569
+ elif ja_count / total > 0.3:
570
+ return "ja" if cn_count / total < 0.3 else "zh-ja"
571
+ elif en_count / total > 0.5:
572
+ return "en"
573
+ else:
574
+ return "mixed"
575
+
576
+
577
+ # ── 路由推荐 ──────────────────────────────────────────────────────────────────
578
+
579
+ def recommend_routing(analysis: dict[str, Any]) -> dict[str, Any]:
580
+ """基于意图分析推荐搜索路由。"""
581
+ strategy = analysis["recommended_strategy"]
582
+
583
+ routing = {
584
+ "strategy": strategy,
585
+ "engines": [],
586
+ "mode": "auto",
587
+ "explanation": "",
588
+ }
589
+
590
+ if strategy == "clarify_first":
591
+ routing["engines"] = ["clarify_first"]
592
+ routing["explanation"] = f"检测到歧义词「{analysis['ambiguities'][0]['term']}」,建议先消歧再搜索"
593
+ routing["mode"] = "auto"
594
+
595
+ elif strategy == "deep_research":
596
+ routing["engines"] = ["research"]
597
+ routing["explanation"] = "检测到深度研究意图,建议使用深度研究工具"
598
+ routing["mode"] = "deep"
599
+
600
+ elif strategy == "split_search":
601
+ routing["engines"] = ["search", "search"] # 两次搜索
602
+ routing["explanation"] = "检测到对比意图,建议分别搜索各对象后对比"
603
+ routing["mode"] = "auto"
604
+
605
+ elif strategy == "news_priority":
606
+ routing["engines"] = ["byted", "uapi", "duckduckgo"]
607
+ routing["explanation"] = "检测到新闻/动态意图,优先使用新闻引擎"
608
+ routing["mode"] = "fast"
609
+
610
+ elif strategy == "direct_search":
611
+ routing["engines"] = ["auto"]
612
+ routing["explanation"] = "事实查询,直接搜索即可"
613
+ routing["mode"] = "fast"
614
+
615
+ else:
616
+ # 根据实体和意图推荐引擎
617
+ entities = [e["type"] for e in analysis["entities"]]
618
+ intents = [i["type"] for i in analysis["intents"]]
619
+
620
+ if "stock_code" in entities or "finance" in [a.get("domain") for a in analysis.get("ambiguities", [])]:
621
+ routing["engines"] = ["eastmoney", "anysearch"]
622
+ routing["explanation"] = "检测到金融实体,优先使用金融引擎"
623
+ elif "cve" in entities:
624
+ routing["engines"] = ["anysearch"]
625
+ routing["explanation"] = "检测到 CVE 编号,使用安全垂直域"
626
+ elif "search_tech" in intents:
627
+ routing["engines"] = ["github", "duckduckgo", "uapi"]
628
+ routing["explanation"] = "检测到技术意图,优先技术源"
629
+ elif "search_opinion" in intents:
630
+ routing["engines"] = ["zhihu", "byted"]
631
+ routing["explanation"] = "检测到观点/评价意图,优先知乎"
632
+ else:
633
+ routing["engines"] = ["auto"]
634
+ routing["explanation"] = "无特殊意图,使用默认路由"
635
+
636
+ return routing
637
+
638
+
639
+ # ── CLI ───────────────────────────────────────────────────────────────────────
640
+
641
+ def main():
642
+ parser = argparse.ArgumentParser(description="意图消歧工具")
643
+ parser.add_argument("query", help="搜索查询")
644
+ parser.add_argument("--explain", action="store_true", help="详细解释")
645
+ parser.add_argument("--json", action="store_true", help="JSON 输出")
646
+ args = parser.parse_args()
647
+
648
+ analysis = analyze_query(args.query)
649
+ routing = recommend_routing(analysis)
650
+ analysis["routing"] = routing
651
+
652
+ if args.json:
653
+ print(json.dumps(analysis, ensure_ascii=False, indent=2))
654
+ else:
655
+ print(f"\n查询分析:{analysis['query']}")
656
+ print(f"语言:{analysis['language']} | 置信度:{analysis['confidence']:.2f}")
657
+ print(f"推荐策略:{routing['strategy']}")
658
+ print(f"说明:{routing['explanation']}")
659
+ print()
660
+
661
+ if analysis["ambiguities"]:
662
+ print("── 歧义检测 ──")
663
+ for a in analysis["ambiguities"]:
664
+ print(f" 「{a['term']}」→ {a['confidence']:.0%} 置信度")
665
+ for m in a["possible_meanings"]:
666
+ marker = "→" if m == a["top_choice"] else " "
667
+ print(f" {marker} {m}")
668
+ print()
669
+
670
+ if analysis["intents"]:
671
+ print("── 意图分类 ──")
672
+ for i in analysis["intents"]:
673
+ print(f" • {i['label']}")
674
+ print()
675
+
676
+ if analysis["entities"]:
677
+ print("── 实体识别 ──")
678
+ for e in analysis["entities"]:
679
+ print(f" • {e['text']} ({e['type']})")
680
+ print()
681
+
682
+ if routing["engines"] and routing["engines"] != ["auto"]:
683
+ print(f"── 推荐引擎 ──")
684
+ print(f" {', '.join(routing['engines'])}")
685
+ print()
686
+
687
+
688
+ if __name__ == "__main__":
689
+ main()