
1. 项目概述从“调参侠”到系统构建者的实战跃迁我带过不少刚入行的工程师也常在技术社区看到类似的问题“模型准确率上不去怎么办”“为什么我的BERT微调结果比论文差一大截”——这些问题本身没错但背后暴露了一个普遍被忽视的事实绝大多数人学AI是从模型开始学的而真正做AI却是从模型之外开始做的。这篇文章讲的不是怎么调出SOTA指标而是我在过去三年里亲手搭过17个生产级AI系统后反复验证、不断推倒重来、最终沉淀下来的八类“非模型但决定成败”的核心工具链。它们不是锦上添花的插件而是让AI从Jupyter Notebook里的玩具变成每天凌晨三点还在稳定响应用户请求的后台服务的关键支点。关键词里提到的“Towards AI - Medium”其实恰恰点出了一个行业现状大量优质内容散落在Medium、Dev.to、个人博客这些平台但它们往往只讲“是什么”和“怎么跑通”极少有人愿意花时间解释“为什么必须用这个而不是那个”“当它在凌晨两点崩掉时你第一眼该看哪三行日志”“为什么Dockerfile里少写一个--no-cache-dir上线后会多花47分钟拉镜像”。这篇文章就是为了解决这些“没人说但天天踩”的问题而写的。它适合两类人一类是已经能跑通Hugging Face示例代码但一上真实业务就卡在数据不一致、API超时、部署失败上的中级实践者另一类是技术负责人正为团队重复造轮子、模型更新后服务不可用、跨环境效果漂移等问题头疼。它不教你怎么写Transformer但会告诉你当你把一个微调好的LLM封装成API时Gradio只是原型阶段的拐杖而真正扛住每秒200次并发查询的是经过6次压测调优的FastAPI Uvicorn Nginx反向代理三层结构——以及为什么这三层缺一不可。我试过用Flask替代FastAPI结果在300并发下平均延迟飙升到2.3秒我也试过跳过Docker直接裸机部署结果客户环境里Python版本冲突导致整个服务启动失败排查了9小时才发现是系统自带的Python 3.8和我们要求的3.10不兼容。这些代价换来的经验现在都浓缩在这八个工具的选择逻辑、配置细节和避坑清单里。它们不是按流行度排序的“2025年最火AI工具Top 10”而是按我在真实项目中遭遇问题的频次和严重程度排列的——排在第一位的永远是那个让我在客户现场改了三遍才通过验收的环节。2. 工具链全景图为什么是这八个而不是其他2.1 核心设计哲学拒绝“模型中心主义”很多团队的AI项目流程图画出来是这样的数据→清洗→特征工程→模型训练→评估→部署。看起来很完整但实际执行时90%的延期和故障都发生在箭头之间——也就是那些被简写为“→”的灰色地带。比如“数据→清洗”这一步真实场景里可能是爬虫定时从5个不同结构的网站抓取HTML其中2个网站加了反爬JS渲染1个需要登录态维持抓取后的HTML要提取正文、过滤广告、归一化编码再存入MySQL而“模型训练→评估”之间又涉及训练数据版本管理、GPU资源排队、超参实验跟踪、结果自动归档……这些环节没有一行代码调用模型却消耗了团队70%以上的工时。所以我选这八个工具的根本逻辑不是“它们有多酷”而是“它们精准覆盖了模型生命周期中最脆弱、最易出错、最影响交付节奏的八个断点”。它们共同构成了一条“抗干扰、可追溯、易协作、能伸缩”的AI系统流水线。下面这张表不是功能罗列而是按项目阶段映射的“救命索引”项目阶段典型痛点对应工具它解决的不是“能不能”而是“稳不稳、快不快、查不查得到”数据获取网站结构变动导致爬虫全崩API限流无感知挂死Scrapy Requests-HTML Apify提供结构化重试、动态渲染、限流熔断、变更告警能力数据预处理同一字段在不同批次数据中类型不一致str/float混用Pandas Dask Great Expectations内置数据契约Data Contract自动校验schema异常数据隔离入库而非报错中断向量存储与检索百万级文档搜索响应超2秒新增文档后旧查询结果不准Qdrant LanceDBQdrant专注高并发低延迟LanceDB专注嵌入式轻量与ACID事务按场景二选一RAG系统构建检索结果相关性低大模型幻觉严重上下文长度溢出LlamaIndex LangChain仅Router模块LlamaIndex的Node Parser对PDF/Word解析更鲁棒LangChain Router用于多源路由决策工作流编排数据更新后模型未自动重训A任务失败导致B任务空转Prefect Airflow混合使用Prefect处理实时事件驱动如新数据入库触发Airflow处理固定周期调度如每日02:00全量重训快速原型验证客户想看效果但后端API还没写完内部演示需支持多用户Gradio Streamlit分工明确Gradio做极简API包装10行代码起Streamlit做带状态管理的交互式分析面板容器化与部署开发环境跑得好测试环境报错GPU驱动版本不一致Docker NVIDIA Container ToolkitDockerfile分层缓存策略基础镜像统一管理规避90%环境差异问题监控与可观测性模型性能缓慢下降无人知某类用户查询延迟突增难定位Prometheus Grafana OpenTelemetry自定义指标token生成速率、embedding耗时、RAG检索召回率、缓存命中率这个选择不是拍脑袋定的。比如向量数据库我对比过Weaviate、Milvus、Chroma、Pinecone、Qdrant、LanceDB六款主流工具在三个维度打分1百万级数据下的P99延迟实测Qdrant在AWS c5.4xlarge上为112msLanceDB为89ms2Schema变更灵活性LanceDB支持ALTER TABLEQdrant需重建collection3运维复杂度Qdrant单二进制部署Milvus需K8s集群。最终结论是Qdrant适合高并发在线服务LanceDB适合离线分析与小规模嵌入式场景。这种基于真实压测数据的取舍才是工具选型的核心依据而不是看GitHub Star数。提示不要迷信“全栈方案”。LangChain曾试图包揽一切结果是每个模块都不够深。现在更务实的做法是“乐高式组合”用LlamaIndex做数据加载与索引用SentenceTransformers做Embedding用Qdrant存向量用FastAPI暴露接口——每个环节都用该领域最专精的工具再用清晰的接口协议如OpenAPI规范连接它们。这样当Qdrant升级到v2.0时你只需改3行客户端代码而不是重构整个LangChain链路。2.2 工具演进逻辑从“能用”到“好用”再到“不得不”这八个工具不是静态清单而是我团队工具链演进的三个阶段切片第一阶段能用2021年我们用Requests写爬虫用Pandas做清洗用SQLite存向量用Flask搭API。能跑通但每次数据源变就要手动改代码每次模型更新就要人工重启服务每次客户问“上周三下午的查询为什么慢”我们只能翻日志大海捞针。第二阶段好用2022年引入Airflow做调度Docker做环境隔离Gradio做原型。自动化程度提升但Airflow DAG写起来像写天书Docker镜像动辄2GBGradio的UI定制成本高。这时我们意识到工具要“好用”必须降低认知负荷——Airflow的DAG应该用YAML声明而非Python编码Docker镜像要分层优化Gradio要能复用React组件。第三阶段不得不2023年至今Qdrant替代SQLitePrefect替代部分AirflowLanceDB用于离线分析Prometheus监控成为标配。这不是因为它们“新”而是因为业务规模到了临界点当单日处理数据从10GB涨到2TB当并发请求从50QPS涨到800QPS当团队从3人扩展到12人跨时区协作时旧工具链的隐性成本调试时间、沟通成本、故障恢复时间已远超学习新工具的成本。这时“不得不换”就成了唯一理性选择。这个演进过程本质上是将隐性知识显性化、将人工操作自动化、将经验判断规则化的过程。比如“数据清洗”这个动作早期靠工程师凭经验写Pandas代码后来用Great Expectations定义数据契约把“订单金额必须0且为数字”写成可执行、可测试、可告警的规则现在这套规则已集成进CI/CD流水线任何数据提交前自动校验不通过则阻断发布。工具的价值从来不在它多炫酷而在它能否把人的经验固化成系统的能力。3. 核心工具深度解析不只是安装更是“为什么这么配”3.1 数据采集从“能爬到”到“爬得稳、爬得准、爬得合规”真实世界的数据源远比教程里的requests.get(https://example.com)复杂。我负责的一个电商价格监控项目需要从京东、淘宝、拼多多、抖音小店、小红书五个平台抓取商品价格。表面看是五个爬虫实际挑战是反爬对抗京东用WebDriver检测淘宝用滑块验证码拼多多用字体混淆抖音小店需模拟APP签名小红书有设备指纹。结构异构同一商品在京东是JSON API返回淘宝是渲染后DOM提取拼多多是WebSocket流式推送。合规红线必须遵守robots.txt设置合理User-Agent和请求间隔关键页面添加meta namerobots contentnoindex, nofollow识别对X-Robots-Tag响应头做解析。我们最终采用的组合是Scrapy作为主框架 Requests-HTML处理JS渲染 Apify作为备用通道。Scrapy提供强大的中间件机制和分布式支持Requests-HTML内嵌Pyppeteer能真实执行JS并提取渲染后DOMApify则是我们的“保险丝”——当自建爬虫因平台策略更新大面积失效时可快速切换到Apify的托管云爬虫用其成熟的反爬绕过能力争取修复时间。关键配置细节# scrapy/settings.py 关键参数 # 避免被封IP的核心策略 DOWNLOAD_DELAY 3 # 基础延迟3秒 RANDOMIZE_DOWNLOAD_DELAY True # 实际延迟在1.5-4.5秒间随机 CONCURRENT_REQUESTS 1 # 单域名并发1严格遵守robots.txt CONCURRENT_REQUESTS_PER_DOMAIN 1 # 中间件自动处理常见反爬头 class AntiCrawlerMiddleware: def process_request(self, request, spider): # 随机User-Agent池 ua_list [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15 ] request.headers[User-Agent] random.choice(ua_list) # 模拟人类操作间隔 time.sleep(random.uniform(0.5, 2.0))注意Requests-HTML的render()方法默认超时5秒但在高负载服务器上可能因Chrome进程启动慢而失败。实测解决方案是1预启动Chrome实例并复用2设置timeout153捕获TimeoutError并降级为纯HTML解析。这三步做完JS渲染成功率从82%提升到99.3%。另一个常被忽略的点是数据溯源与审计。我们要求每条爬取数据必须附带元信息{ source_url: https://item.jd.com/1000XXXXXX.html, crawl_timestamp: 2025-03-15T14:22:33.123Z, html_snapshot_hash: sha256:abc123..., extractor_version: v2.1.4, proxy_used: us-east-1-residential-001 }这些字段不是为了好看而是当客户质疑“你们数据不准”时能立刻回溯到原始HTML快照用diff工具比对提取逻辑是否变更。这避免了90%的扯皮。3.2 数据预处理用“契约”代替“信任”用“隔离”代替“报错”Pandas是数据清洗的瑞士军刀但它的强大也埋下了隐患df[price].astype(float)在遇到N/A时默认转成NaN而NaN在后续计算中会悄无声息地污染结果。我们吃过亏——一个金融风控模型因某批数据中income字段混入字符串NULL导致所有income 50000的判断全部失效上线三天才被发现。解决方案是Great ExpectationsGE。它不取代Pandas而是给Pandas加上“契约”Data Contract。核心思想在数据进入Pipeline前先定义“它应该长什么样”再用GE自动验证。典型工作流定义期望Expectation Suite# expectations/orders_suite.json { expectations: [ { expectation_type: expect_column_values_to_be_of_type, kwargs: {column: order_id, type_: string} }, { expectation_type: expect_column_values_to_be_between, kwargs: {column: amount, min_value: 0.01, max_value: 999999.99} }, { expectation_type: expect_column_values_to_match_regex, kwargs: {column: email, regex: ^[a-zA-Z0-9._%-][a-zA-Z0-9.-]\\.[a-zA-Z]{2,}$} } ] }验证并隔离异常import great_expectations as ge from great_expectations.core import ExpectationSuite context ge.data_context.DataContext() suite context.get_expectation_suite(orders_suite) validator context.sources.pandas_default.read_csv(raw_orders.csv) results validator.validate(expectation_suitesuite) # 自动将异常数据写入quarantine目录正常数据继续流程 if not results.success: quarantine_df validator.head(1000)[~validator.get_failed_rows()] quarantine_df.to_parquet(data/quarantine/orders_20250315.parquet) # 发送告警Slack通知邮件摘要GE的价值在于它把“数据质量”从主观判断变成了客观事实。当新同事加入他不需要读几百页文档理解“amount字段为什么不能为负”只需看expect_column_values_to_be_between这条规则。当数据源变更GE会在第一行数据就报错而不是让错误潜伏到模型训练阶段。实操心得GE的expect_column_pair_values_to_be_equal对关联数据一致性检查极有用。比如订单表和支付表用order_id关联时GE能验证“所有订单表中的order_id在支付表中都有对应记录”这比写SQLLEFT JOIN再查IS NULL直观十倍。3.3 向量数据库选型不是比参数而是比“谁更懂你的场景”Qdrant和LanceDB常被拿来对比。但我的经验是它们根本不是竞品而是互补的搭档。就像锤子和螺丝刀不存在“哪个更好”只存在“此刻拧螺丝还是敲钉子”。Qdrant我们用它承载所有在线服务的向量检索。原因很实在它的HNSW索引在百万级数据下P99延迟稳定在150ms内实测AWS r6i.2xlarge 100GB EBSpayload字段支持全文检索与向量检索混合比如filter: {must: [{key: category, match: {value: laptop}}]}scrollAPI支持海量数据导出recommendAPI原生支持协同过滤。关键配置优化# qdrant_config.yaml storage: # 启用mmap提升IO性能 mmap_threshold_kb: 1048576 # 1GB # 避免频繁刷盘影响查询 sync_interval_sec: 60 # 内存限制防OOM max_memory_ratio: 0.5LanceDB我们用它做离线分析和小规模POC。优势在于单文件存储.lance目录Git可追踪git diff能看清向量变化内置ALTER TABLE增加新列无需重建整个库Python API极简db.create_table(docs, datadf)一行创建。典型场景客户要验证“用新Embedding模型替换旧模型效果提升多少”。我们用LanceDB快速建两个表docs_old/docs_new用相同查询集测试git commit -m v2 embedding: 2.3% recall5结果一目了然。注意Qdrant的quantization量化功能虽能减小内存但会牺牲精度。我们在生产环境关闭它因为“多占2GB内存”比“召回率下降0.5%”代价小得多。这是典型的业务权衡——技术参数必须服务于业务目标。3.4 RAG系统构建抛弃“链式思维”拥抱“节点式架构”LangChain的SequentialChain曾让我着迷但真实项目很快打脸。一个法律咨询RAG系统需要同时处理1用户问题分类刑事/民事/行政2从法规库检索相关法条3从案例库检索相似判例4调用LLM生成回答。如果硬套SequentialChain一旦步骤2检索失败整个链路就中断。我们转向LlamaIndex的Node-Link架构。核心思想数据不是线性流动而是以“节点”Node形式存在节点间通过“关系”Relationship连接。每个节点可以独立处理、缓存、监控。架构示意[User Query] ↓ (Query Engine) [Query Router] → 分类 → [Criminal Law Index] → [Retriever] → [Nodes:刑法第232条...] ↓ → 分类 → [Civil Law Index] → [Retriever] → [Nodes:民法典第1165条...] ↓ → 分类 → [Case Index] → [Retriever] → [Nodes:2023京0101民初123号...] ↓ [Response Synthesizer] ← 聚合所有Nodes内容 ← LLM关键代码片段简化from llama_index.core import VectorStoreIndex, StorageContext from llama_index.vector_stores.qdrant import QdrantVectorStore from llama_index.core.retrievers import RouterRetriever from llama_index.core.selectors import LLMSingleSelector # 构建多个独立索引 criminal_index VectorStoreIndex.from_vector_store( QdrantVectorStore(clientclient, collection_namecriminal_laws) ) civil_index VectorStoreIndex.from_vector_store( QdrantVectorStore(clientclient, collection_namecivil_laws) ) # RouterRetriever自动选择最相关索引 retriever RouterRetriever.from_defaults( retriever_dict{ criminal: criminal_index.as_retriever(), civil: civil_index.as_retriever(), }, selectorLLMSingleSelector.from_defaults(), verboseTrue, )这种架构的好处是故障隔离。当刑事法规库因网络问题无法访问RouterRetriever会自动降级到民事库或案例库而不是让整个服务返回500错误。可观察性增强每个Retriever的retrieve()方法返回NodeWithScore列表我们可以记录每个节点的score、node_id、text在Grafana里画出“各来源召回质量热力图”。实操心得LlamaIndex的NodeParser对PDF解析比LangChain鲁棒得多。它能自动识别PDF中的标题层级、表格、图片caption并生成带metadata的Node。我们曾用LangChain解析一份120页的《医疗器械监督管理条例》结果表格全乱码换成LlamaIndex的HierarchicalNodeParser一页PDF生成23个语义清晰的Node准确率98.7%。4. 实操全流程从零搭建一个生产级RAG服务4.1 环境准备与依赖管理告别“在我机器上能跑”第一步永远是最容易被跳过的却是故障率最高的环节。我们团队的黄金法则所有环境必须可重现、可审计、可回滚。Python环境不用pip install -r requirements.txt而用pip-tools生成锁定文件# pyproject.toml 定义高层依赖 [tool.poetry.dependencies] python ^3.10 llama-index ^0.10.0 qdrant-client ^1.7.0 # 生成精确锁定 pip-compile pyproject.toml --output-filerequirements.txt # 输出 requirements.txt 包含llama-index0.10.3, qdrant-client1.7.2, ...Docker基础镜像不使用python:3.10-slim而用我们自己维护的ai-base:3.10-cu118预装CUDA 11.8 PyTorch 2.1 cuDNN。原因slim镜像每次都要apt-get update apt-get install -y ...耗时且不稳定自建镜像首次构建慢但后续所有服务都复用CI/CD提速40%。配置管理所有密钥、API地址、超时参数不写死在代码里而用pydantic-settings管理from pydantic_settings import BaseSettings class Settings(BaseSettings): QDRANT_URL: str QDRANT_API_KEY: str OPENAI_API_KEY: str EMBEDDING_MODEL: str text-embedding-3-small # 自动从环境变量、.env文件、命令行参数加载 class Config: env_file .env env_file_encoding utf-8提示.env文件绝不提交到Git。我们用pre-commit钩子检查fail: grep -q API_KEY\|PASSWORD .env。同时CI/CD中所有密钥通过Secret Manager注入确保本地开发和生产环境配置完全一致。4.2 数据管道构建从原始PDF到可检索向量以一个企业知识库项目为例原始数据是200份PDF格式的《员工手册》《安全规程》《IT政策》。目标让用户输入“如何报销差旅费”系统返回手册中相关章节。Step 1PDF解析与分块from llama_index.core import SimpleDirectoryReader from llama_index.core.node_parser import HierarchicalNodeParser # 自动识别PDF结构 documents SimpleDirectoryReader( input_dir./policies/, required_exts[.pdf], filename_as_idTrue, ).load_data() # 分层解析按标题层级切分保留父子关系 node_parser HierarchicalNodeParser.from_defaults( chunk_sizes[2048, 512, 128], # 大块章节、中块小节、小块段落 ) nodes node_parser.get_nodes_from_documents(documents) # 每个Node自动携带metadata for node in nodes[:3]: print(fID: {node.id_}, Parent: {node.parent_node_id}, Text: {node.text[:100]}...)实测效果一份50页的《信息安全政策》生成142个Node其中parent_node_id为空的是顶级章节如“1. 总则”有值的是子小节如“1.2 访问控制”完美保留语义结构。Step 2向量化与入库from llama_index.embeddings.openai import OpenAIEmbedding from llama_index.vector_stores.qdrant import QdrantVectorStore from qdrant_client import QdrantClient # 初始化Qdrant客户端生产环境用HTTPSAPI Key client QdrantClient( urlsettings.QDRANT_URL, api_keysettings.QDRANT_API_KEY, timeout30, ) # 创建collection指定HNSW索引参数 client.recreate_collection( collection_namepolicies, vectors_configmodels.VectorParams( size1536, # text-embedding-3-small输出维度 distancemodels.Distance.COSINE, ), # HNSW关键参数平衡精度与速度 hnsw_configmodels.HnswConfigDiff( m16, # 每个节点的邻居数越大精度越高内存越多 ef_construct100, # 构建时搜索深度越大越准但越慢 ), ) # 批量插入1000条/批避免超时 vector_store QdrantVectorStore(clientclient, collection_namepolicies) storage_context StorageContext.from_defaults(vector_storevector_store) index VectorStoreIndex(nodes, storage_contextstorage_context)关键参数说明m16是Qdrant官方推荐的平衡值ef_construct100意味着构建索引时每个向量会搜索100个最近邻来建立连接这比默认值64更准但构建时间增加约35%。我们接受这个代价因为索引只构建一次而查询要执行百万次。Step 3检索增强生成RAG服务from llama_index.core import Settings from llama_index.llms.openai import OpenAI # 配置LLM生产环境必设超时和重试 Settings.llm OpenAI( modelgpt-4-turbo, temperature0.1, # 降低幻觉 timeout60, # 防止LLM卡死 max_retries3, # 网络抖动自动重试 ) # 构建Query Engine query_engine index.as_query_engine( # 检索参数返回top_k5个最相关Node similarity_top_k5, # RAG核心用检索到的Node内容作为上下文 response_modecompact, # 自动压缩上下文避免超长 ) # 测试查询 response query_engine.query(员工出差住宿标准是多少) print(response.response) # 输出根据《差旅费用管理办法》第3.2条一线城市住宿标准为800元/天...4.3 服务部署与监控让AI系统像水电一样可靠Docker化部署# Dockerfile FROM ai-base:3.10-cu118 # 复制锁定依赖利用Docker layer cache COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制应用代码 COPY app/ /app/ WORKDIR /app # 生产级启动Uvicorn Gunicorn CMD [gunicorn, -w, 4, -k, uvicorn.workers.UvicornWorker, --bind, 0.0.0.0:8000, --timeout, 120, main:app]关键点-w 4开4个worker--timeout 120防止长查询阻塞uvicorn.workers比纯Uvicorn更健壮能自动重启崩溃的worker。Prometheus监控指标# main.py 中添加自定义指标 from prometheus_client import Counter, Histogram, Gauge # 请求计数器 QUERY_COUNT Counter(rag_query_total, Total number of RAG queries) # 延迟直方图单位秒 QUERY_LATENCY Histogram(rag_query_latency_seconds, RAG query latency, buckets[0.1, 0.5, 1.0, 2.0, 5.0, 10.0]) # 当前活跃查询数 ACTIVE_QUERIES Gauge(rag_active_queries, Number of currently active queries) app.middleware(http) async def add_metrics(request: Request, call_next): QUERY_COUNT.inc() ACTIVE_QUERIES.inc() start_time time.time() try: response await call_next(request) return response finally: duration time.time() - start_time QUERY_LATENCY.observe(duration) ACTIVE_QUERIES.dec()在Grafana中我们看三个核心看板P95延迟趋势超过2秒标红触发告警缓存命中率Qdrant的cache_hits指标低于95%说明索引效率下降LLM token生成速率openai_completion_tokens_total突降说明API限流或模型异常。实操心得我们给每个query_engine.query()调用加了trace_id并记录到OpenTelemetry。当某个查询慢可在Jaeger里看到完整链路HTTP → FastAPI → Qdrant检索 → LLM调用 → 响应每一环节耗时精确到毫秒。这让我们在30分钟内定位到一个性能瓶颈Qdrant的ef_search参数设得太小默认16导致检索时只查16个邻居召回率低LLM被迫生成更多token来“猜答案”最终延迟飙升。调大到64后P95延迟从3.2秒降到0.8秒。5. 常见问题与排查技巧实录那些没写在文档里的坑5.1 数据管道故障当爬虫突然不工作了现象某天凌晨2点Airflow DAG显示“PriceSpider”任务失败日志只有HTTP 403 Forbidden。排查路径确认是否全局问题curl同一个URL返回403但加-H User-Agent: Mozilla/5.0...后成功 → 确认是UA被封。检查UA池发现UA列表里还用着2023年的Chrome版本 → 平台升级了UA检测规则。根因京东更新了反爬策略要求UA必须包含Chrome/120.0.0.0而我们池子里最新是Chrome/115。解决方案立即更新UA池10分钟在Scrapy中间件里加retry_times3每次重试换新UA长期接入Apify的web-scraperactor用其自动UA轮换能力。注意不要只修bug要建防御。我们在Airflow DAG里加了“健康检查任务”每天01:00用最新UA访问所有目标网站首页返回非200则发Slack告警。这让我们在问题发生前就收到预警。5.2 向量检索失准为什么明明相关的内容没被搜到现象用户搜“苹果手机维修”返回结果是“iPhone 15 Pro维修指南”但漏掉了同样相关的“Apple授权服务中心列表”。根因分析Embedding模型偏差text-embedding-3-small对品牌名“Apple”和“苹果”的向量距离较远中文分词 vs 英文token检索策略缺陷只用了向量相似度没结合关键词匹配。双管齐下修复混合检索Hybrid Search# Qdrant支持BM25 向量混合 search_result client.search( collection_namepolicies, query_vectorembed_query(苹果手机维修), query_filtermodels.Filter( must[models.FieldCondition(keystatus, matchmodels.MatchValue(valueactive))] ), # 混合0.7向量 0.3关键词 search_paramsmodels.SearchParams(hybrid_fusionmodels.HybridFusion.RELATIVE_SCORE), limit10, )Query重写Query Rewriting# 用LLM自动扩展同义词 rewrite_prompt f 请为用户查询生成3个语义等价的变体用逗号分隔 用户查询{user_query} 例如买苹果手机 → 购买iPhone,购入苹果手机,入手iPhone rewritten_queries llm.complete(rewrite_prompt).text.split(,) # 对每个