1. 这不是又一篇“AI Agent工具罗列”,而是一份开发者亲手踩坑后画出的选型地图
你打开浏览器搜“AI Agent 互联网检索工具”,页面刷出来几十篇标题雷同的文章:《2024最火AI Agent工具TOP10》《5款开源Agent框架横向对比》《一文看懂RAG与Agent区别》……点开一看,全是截图堆砌+参数表格+“各有优劣”的万金油结论。我试过其中7个标榜“开箱即用”的方案,有3个在本地跑通demo后,一接入真实业务场景就卡在超时重试上;有2个文档里写着“支持动态网页解析”,结果连带登录态的新闻后台都抓不到正文;还有1个号称“零代码配置”,实际要手写YAML定义17个节点的执行顺序——最后发现,它根本没处理JavaScript渲染的SPA页面,爬回来的全是空div。
这根本不是工具不行,而是绝大多数选型指南缺了一块关键拼图:没有把“互联网检索”这个动作本身,拆解成开发者真正要面对的技术断层。不是“能不能搜”,而是“搜什么内容”“怎么保持会话上下文”“如何应对反爬策略升级”“怎样把非结构化结果喂给LLM做推理”——这些环节环环相扣,任何一个断点都会让整个Agent流程崩掉。我过去两年带团队落地了6个面向B端客户的AI Agent项目,其中4个在检索模块反复重构超过3轮,最终沉淀出一套可复用的决策树:先判断你的数据源是静态HTML、动态渲染页、还是需要登录态的私有API;再匹配对应的数据获取方式(requests+bs4、Playwright、Puppeteer、或直接调用平台官方SDK);最后决定是否引入缓存层、代理池、或自定义解析器。这篇文章不给你列工具清单,而是带你从HTTP请求头开始,一层层剥开互联网检索在AI Agent架构里的真实肌理。适合正在写第一个Agent demo的新人,也适合被线上故障逼到凌晨三点改重试逻辑的资深工程师。
2. 为什么“互联网检索”是AI Agent最脆弱的神经末梢?
2.1 检索不是搬运工,而是Agent的感官系统
很多人把AI Agent的互联网检索简单理解为“调用搜索引擎API”,这就像说“人的眼睛只是把光信号传给大脑”。实际上,检索模块承担着Agent的环境感知、信息过滤、上下文锚定三重职能。举个真实案例:某电商客服Agent需要实时比价,用户问“iPhone 15 Pro现在京东和天猫哪个便宜?”。表面看只需抓取两个页面价格,但实际要处理:
- 动态加载陷阱:京东商品页价格藏在
<script>标签的JSON里,天猫则用WebAssembly加密计算优惠券叠加逻辑; - 会话状态依赖:用户刚在对话中说“我在上海”,检索时需自动注入地域参数,否则返回的“满300减50”可能不适用;
- 时效性悖论:价格变动频繁,但Agent不能每秒刷新,需设计缓存失效策略(如价格变动>5%才更新);
- 语义歧义消解:“便宜”指单价低?还是折后总价低?或是含运费的到手价?检索前必须完成意图澄清。
这些都不是SDK能自动解决的。我见过团队用Serper API直接返回搜索结果,结果Agent把“iPhone 15 Pro Max”页面的价格当成“Pro”型号报价,因为API返回的标题没做实体识别。真正的检索模块,必须像人类一样具备上下文感知力、异常预判力、结果校验力——它不是管道,而是带思考能力的传感器。
2.2 主流工具链的三大技术断层
当前所有AI Agent框架(LangChain、LlamaIndex、Semantic Kernel)都把检索抽象成Retriever接口,但底层实现暴露三个致命断层:
断层一:协议层与渲染层的割裂
多数框架默认使用requests库,它只能获取HTML源码。但现代网页90%以上采用React/Vue构建,核心内容由JS动态渲染。当你用requests.get(url).text拿到的页面,实际是未执行JS的骨架,真实数据还在XHR请求里。我们曾用LangChain的WebBaseLoader抓取知乎文章,返回内容全是“正在加载中...”,因为框架没集成浏览器自动化能力。解决方案必须分层:静态页用requests+BeautifulSoup,动态页必须用Playwright或Selenium启动无头浏览器,且要配置等待策略(如page.wait_for_selector('.content')而非固定sleep)。
断层二:反爬策略的被动响应
框架文档常写“支持User-Agent轮换”,但真实反爬远不止于此。某招聘网站检测到Playwright指纹后,会返回虚假职位列表;某财经平台对Headless Chrome返回403,但对真实Chrome 120版本放行。我们测试发现,仅靠修改user-agent字段成功率不足35%,必须组合:
- 浏览器指纹伪装(修改
navigator.webdriver、plugins长度等) - 请求头精细化(
sec-ch-ua需匹配真实Chrome版本) - 行为模拟(鼠标移动轨迹、滚动延迟)
- IP代理池(单IP日请求超200次必封)
这些操作无法通过框架配置项一键开启,必须侵入底层Driver实例。
断层三:结果结构化的不可靠性
框架提供的Html2Text或Unstructured解析器,在遇到复杂DOM时极易失效。比如某政府网站公告页,价格信息分散在<table>、<ul>、<p>三种标签中,且同一段文字内混用<strong>和<span style="color:red">强调价格。我们实测unstructured.partition_html对这类页面的提取准确率仅62%,错误包括:
- 将“¥5,999”识别为“¥5999”(丢失千分位逗号)
- 把“活动截止:2024-03-15”误判为价格“2024-03-15”
- 合并相邻
<p>导致“原价¥6999”和“现价¥5999”变成“原价¥6999现价¥5999”
这迫使我们在解析后增加规则引擎校验:用正则匹配货币符号、日期格式,用词性分析区分价格与时间。
提示:别迷信“开箱即用”的检索组件。真正的生产级Agent,检索模块代码量往往占整体30%以上,因为它要直面互联网的混沌本质——没有标准协议,只有不断演化的对抗策略。
3. 四类典型场景的选型决策树与实操验证
3.1 场景一:静态资讯聚合(如新闻监控、政策追踪)
核心特征:目标网站为传统CMS架构(WordPress/DedeCMS),内容以静态HTML发布,无登录态,更新频率低(小时级)。
选型逻辑:优先选择轻量级、高并发、易维护方案。放弃浏览器自动化,因其启动开销大(单次请求平均耗时2.3s vs requests的0.4s),且静态页无需JS渲染。
实操验证:我们对比了三种方案在抓取100个政府官网页面的性能:
| 方案 | 平均耗时 | 成功率 | 内存占用 | 维护难度 |
|---|---|---|---|---|
requests+BeautifulSoup | 0.42s | 99.2% | 12MB | ★☆☆☆☆(需手写XPath) |
Scrapy | 0.38s | 98.7% | 85MB | ★★☆☆☆(需写Spider类) |
Playwright(无头模式) | 2.15s | 100% | 320MB | ★★★★☆(配置复杂) |
最终方案:requests+lxml(非BeautifulSoup,因lxml解析速度提升40%)。关键技巧:
- 使用
lxml.html.fromstring()替代BeautifulSoup(),避免HTML解析器自动修复错误标签带来的结构偏移; - 预编译XPath表达式:
price_xpath = etree.XPath('//div[@class="price"]/text()'),比运行时解析快3倍; - 实现智能重试:首次失败后,检查HTTP状态码,若为403则切换User-Agent,若为503则指数退避重试(1s→2s→4s)。
避坑心得:某省政务网启用HTTPS强制跳转后,requests默认不跟随重定向,导致抓取失败。解决方案不是加allow_redirects=True,而是捕获requests.exceptions.TooManyRedirects异常,手动解析Location头获取真实URL——因为该网站重定向链长达7跳,requests默认只跟5跳。
3.2 场景二:动态电商比价(如实时价格监控)
核心特征:目标站为Vue/React SPA,价格数据通过AJAX异步加载,需维持登录态(Cookie/JWT),存在反爬验证码。
选型逻辑:必须使用浏览器自动化,但需规避完整浏览器开销。放弃Selenium(内存泄漏严重),选择Playwright(内存管理更优)或Puppeteer(Node.js生态成熟)。
实操验证:在抓取京东商品页时,我们测试不同等待策略对成功率的影响:
| 策略 | 成功率 | 平均耗时 | 失败原因 |
|---|---|---|---|
page.wait_for_timeout(3000) | 68% | 3.2s | JS未执行完,价格为空 |
page.wait_for_selector('#price .p-price') | 89% | 4.1s | 选择器在DOM中存在但内容为空 |
page.wait_for_function("document.querySelector('#price .p-price').innerText.length > 0") | 97% | 4.8s | 精确等待文本渲染完成 |
最终方案:Playwright + 自定义等待函数。关键配置:
# 启动时禁用图片加载,提速40% browser = await playwright.chromium.launch(headless=True, args=[ '--disable-images', '--disable-gpu', '--no-sandbox' ]) # 设置全局超时,避免单页阻塞 context = await browser.new_context( viewport={'width': 1920, 'height': 1080}, ignore_https_errors=True, java_script_enabled=True ) # 注入防检测脚本 await page.add_init_script(""" Object.defineProperty(navigator, 'webdriver', {get: () => undefined}); window.chrome = {runtime: {}}; """)避坑心得:京东商品页价格常被包裹在<span class="price">¥<em>5,999</em></span>,直接取.text_content()会得到“¥5,999”,但<em>标签内数字可能被CSS隐藏(display:none)。正确做法是:page.eval_on_selector('#price', 'el => el.innerText'),强制获取渲染后文本。
3.3 场景三:私有API数据接入(如企业内部知识库)
核心特征:数据源为公司内网REST API,需OAuth2认证,返回JSON结构化数据,但存在速率限制(100次/分钟)。
选型逻辑:放弃通用爬虫,直接调用API。重点在认证管理、限流控制、错误重试三方面。
实操验证:我们对接某CRM系统的API时,发现其OAuth2令牌有效期仅1小时,但刷新令牌接口要求refresh_token必须在15分钟内使用。若Agent持续运行超1小时,会出现令牌过期错误。解决方案:
- 在每次API调用前检查令牌剩余有效期,若<10分钟则主动刷新;
- 使用
asyncio.Semaphore控制并发数,确保不超过100次/分钟; - 对429错误(Too Many Requests)实施指数退避,同时记录触发时间,动态调整请求间隔。
最终方案:httpx(异步HTTP客户端) +Authlib(OAuth2管理)。关键代码:
class CRMClient: def __init__(self): self.token = None self.token_expiry = 0 self.semaphore = asyncio.Semaphore(5) # 控制并发 async def _ensure_token(self): if time.time() > self.token_expiry - 600: # 提前10分钟刷新 async with httpx.AsyncClient() as client: resp = await client.post( "https://crm.example.com/oauth/token", data={ "grant_type": "refresh_token", "refresh_token": self.refresh_token } ) self.token = resp.json()["access_token"] self.token_expiry = time.time() + resp.json()["expires_in"] async def get_contacts(self, page=1): async with self.semaphore: # 限流 await self._ensure_token() async with httpx.AsyncClient() as client: resp = await client.get( f"https://crm.example.com/api/v1/contacts?page={page}", headers={"Authorization": f"Bearer {self.token}"} ) if resp.status_code == 429: retry_after = int(resp.headers.get("Retry-After", "1")) await asyncio.sleep(retry_after * 2) # 指数退避 return await self.get_contacts(page) return resp.json()避坑心得:某次上线后发现API调用失败率骤升,排查发现是CRM系统升级后,将Retry-After头从秒改为毫秒单位,但我们的代码仍按秒解析。教训:永远检查API文档的变更日志,对关键头字段做单位校验。
3.4 场景四:多源异构数据融合(如竞品分析报告生成)
核心特征:需同时抓取新闻稿(静态HTML)、财报PDF(需OCR)、社交媒体帖子(需登录态)、App Store评论(需iOS模拟器),数据格式差异极大。
选型逻辑:单一工具无法覆盖,必须构建分层架构:
- 接入层:统一调度不同数据源的采集任务;
- 适配层:为每类数据源定制解析器(HTML解析器、PDF OCR引擎、移动端抓取器);
- 归一化层:将不同格式结果映射到统一Schema(如
{source: "weibo", content: "...", timestamp: "2024-03-15"})。
实操验证:我们为某手机厂商搭建竞品分析Agent,需整合5类数据源。测试发现,直接用PyMuPDF解析财报PDF时,对扫描版PDF识别率仅35%。改用Tesseract+OpenCV预处理后提升至89%:
- 先用OpenCV二值化、去噪、旋转矫正;
- 再用Tesseract OCR识别,设置
--psm 6(假设为单栏文本); - 最后用正则提取“营业收入:¥XX.XX亿元”。
最终方案:自研调度框架 + 插件化解析器。架构图:
[调度中心] → [任务队列] → [HTML采集器] → [PDF OCR引擎] → [微博爬虫] → [App Store解析器] ↓ [归一化处理器] → [向量数据库]避坑心得:App Store评论需模拟iOS设备,我们最初用WebDriverAgent,但苹果审核严格,频繁封禁IP。后来改用app-store-scraper库(基于官方API),但发现其返回评论数与App Store显示不符。深挖发现:官方API默认只返回最新100条评论,需循环调用offset参数。教训:永远验证API返回数据的完整性,不要相信文档的“默认值”。
4. 工具链深度对比:不只是功能表,更是工程成本账本
4.1 开源框架选型:LangChain vs LlamaIndex vs Semantic Kernel
| 维度 | LangChain | LlamaIndex | Semantic Kernel |
|---|---|---|---|
| 检索抽象粒度 | Retriever接口宽泛,需自行实现细节 | BaseQueryEngine聚焦文档检索,内置VectorStoreQuery | Kernel抽象为Plugin,检索需封装为Function |
| 动态页支持 | 需额外集成PlaywrightLoader,文档分散 | 原生支持SimpleWebPageReader,但仅限静态页 | 无内置Web加载器,需手写HttpPlugin |
| 缓存机制 | InMemoryCache简单,不支持分布式 | VectorStoreIndex自带向量缓存,但需额外配Redis | 依赖Azure Cache for Redis,云服务绑定强 |
| 调试体验 | debug=True输出详细执行链,但日志冗长 | set_global_handler可定制日志,但缺少步骤耗时统计 | Telemetry模块完善,但需Azure Monitor集成 |
| 社区活跃度 | GitHub Star 62k,但PR合并慢(平均7天) | GitHub Star 38k,Issue响应快(<24h) | GitHub Star 18k,微软官方维护,更新稳定 |
实操结论:
- 快速验证选LangChain:它的
Tool概念最直观,DuckDuckGoSearchRun一行代码就能跑通搜索,适合Demo阶段; - 生产部署选LlamaIndex:它的
ServiceContext可精细控制嵌入模型、LLM、向量存储,且QueryEngine支持SubQuestionQueryEngine自动拆解复杂问题; - 企业级集成选Semantic Kernel:当你的Agent需与Azure AI Studio、Microsoft Graph深度集成时,它的
Plugin体系天然兼容,避免重复造轮子。
注意:别被Star数迷惑。我们曾用LangChain的
SQLDatabaseChain连接MySQL,结果发现它生成的SQL存在SQL注入风险(未参数化),而LlamaIndex的SQLStructStore默认启用参数化查询。选型要看代码质量,不是人气。
4.2 浏览器自动化工具:Playwright vs Puppeteer vs Selenium
| 维度 | Playwright | Puppeteer | Selenium |
|---|---|---|---|
| 多浏览器支持 | Chromium/Firefox/WebKit全支持 | 仅Chromium | 全浏览器,但Firefox驱动不稳定 |
| 反检测能力 | 内置bypass_csp、ignore_https_errors | 需手动注入脚本 | 需第三方库(如selenium-wire) |
| 内存占用 | 单实例约180MB | 单实例约220MB | 单实例约350MB(Java版更高) |
| Python生态 | playwright-python成熟,API一致 | pyppeteer已停止维护,playwright是事实标准 | selenium稳定,但新特性支持慢 |
| 移动端模拟 | device="iPhone 13"一行切换 | emulate方法有限 | 需Appium扩展 |
实操结论:
- 新项目一律用Playwright:它的
locator机制比Selenium的find_element更鲁棒(自动等待元素出现); - 遗留Puppeteer项目不必迁移:若已用
pyppeteer跑通,其稳定性足够,迁移成本高于收益; - Selenium仅用于特殊需求:如必须用IE浏览器(已淘汰)或需
RemoteWebDriver分布式部署。
独家技巧:Playwright的page.screenshot()默认截全屏,但电商页价格常在折叠区域。解决方案:page.locator('#price').screenshot()直接截取目标元素,避免滚动和等待。
4.3 解析与清洗工具:BeautifulSoup vs lxml vs Unstructured
| 工具 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
BeautifulSoup | 语法简单,容错性强(自动修复坏HTML) | 速度慢(纯Python实现),内存占用高 | 快速原型,小规模HTML解析 |
lxml | C语言实现,速度是BS4的5倍,内存占用低 | 需要精确XPath,对坏HTML容忍度低 | 生产环境,大规模静态页解析 |
Unstructured | 支持PDF/DOCX/PPTX等10+格式,内置OCR | 依赖pdfminer和tesseract,安装复杂 | 多格式文档统一处理 |
实操结论:
- HTML解析首选lxml:用
etree.HTMLParser(recover=True)可兼顾速度与容错; - PDF处理用Unstructured:但务必关闭
chunking(strategy="fast"),否则会把“¥5,999”切分成“¥5,”和“999”; - 避免混合使用:曾见团队用BS4解析HTML,再用lxml处理子节点,结果BS4自动修复的标签与lxml的XPath不匹配,导致定位失败。
避坑清单:
lxml解析含中文的HTML时,若未指定编码,会默认用ASCII,导致乱码。解决方案:etree.fromstring(html.encode('utf-8'));Unstructured对扫描PDF的OCR准确率受DPI影响,低于150DPI时识别率暴跌。预处理必须用convert_from_path(pdf_path, dpi=200)。
5. 生产环境避坑指南:那些文档不会写的血泪教训
5.1 反爬对抗的实战军规
军规一:永远不要信任User-Agent字符串
某次我们用fake-useragent库随机UA,结果发现返回的“Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...”被目标站识别为爬虫。深挖发现,该站校验navigator.platform与UA中操作系统是否匹配。解决方案:Playwright中设置user_agent的同时,注入脚本伪造navigator.platform:
await page.add_init_script(`{ Object.defineProperty(navigator, 'platform', {value: 'Win32'}); }`);军规二:IP代理池必须带健康检查
我们采购的代理服务承诺99.9%可用率,但实际测试发现,同一IP在10分钟内对京东返回200,对淘宝返回403。原因:不同网站的IP黑名单独立维护。解决方案:为每个代理IP建立独立健康度评分,根据各网站响应码动态调整权重。
军规三:JavaScript渲染必须等“真”完成
某汽车论坛价格页,document.querySelector('.price')存在,但innerText为空,因为价格由setTimeout延迟1秒填充。wait_for_function虽能解决,但过度等待拖慢性能。终极方案:注入监听脚本,捕获价格变化事件:
await page.add_script_tag(content=""" const priceEl = document.querySelector('.price'); const observer = new MutationObserver(() => { if (priceEl.innerText) { window.priceReady = true; } }); observer.observe(priceEl, {childList: true}); """) await page.wait_for_function("window.priceReady === true")5.2 缓存与重试的黄金法则
法则一:缓存键必须包含上下文哈希
用户问“上海iPhone 15 Pro价格”,若缓存键仅为"iphone_15_pro_price",则北京用户得到错误结果。正确键:f"iphone_15_pro_price_{hashlib.md5('上海'.encode()).hexdigest()[:8]}"。
法则二:重试不是越多越好
对404错误重试10次毫无意义,只会加重服务器负担。我们的重试策略:
- 404/410:立即失败,记录URL失效;
- 429:指数退避,最大等待60秒;
- 500/502/503:线性退避,最多3次;
- 超时:递增超时阈值(10s→15s→20s)。
法则三:缓存穿透防护
恶意请求不存在的商品ID(如/product/999999999),会导致大量请求击穿缓存直达后端。解决方案:布隆过滤器预检,或对空结果也缓存(TTL设为1分钟)。
5.3 数据质量校验的硬核手段
手段一:价格数字标准化
从网页提取的“¥5,999”、“$5999.00”、“5999元”需统一为浮点数。我们用正则提取所有数字字符,再根据货币符号确定小数位:
def parse_price(text): # 提取所有数字和小数点 digits = re.findall(r'[\d.,]+', text) if not digits: return None # 合并连续数字(处理“5,999.00”) clean = re.sub(r'[^\d.]', '', digits[0]) # 根据逗号位置判断千分位 if ',' in clean and '.' in clean: parts = clean.split('.') if len(parts[-1]) == 2: # 小数点后2位,逗号为千分位 clean = clean.replace(',', '') return float(clean)手段二:时效性验证
抓取的新闻发布时间“2024-03-15”可能是伪造的。交叉验证:
- 检查URL路径是否含日期(
/2024/03/15/article.html); - 比对页面
<meta property="article:published_time">; - 计算页面MD5,与历史快照比对(使用Wayback Machine API)。
手段三:来源可信度打分
对同一事件,不同网站报道可信度不同。我们建立简易评分模型:
- 官方网站(.gov/.edu):+3分;
- 头部媒体(人民日报/新华社):+2分;
- 自媒体(微信公众号):-1分;
- 未备案网站:-3分;
- 总分≥2才纳入Agent知识库。
5.4 监控与告警的最小可行方案
监控指标:
- 成功率:
success_count / total_requests,阈值<95%告警; - 平均耗时:
avg_response_time,突增50%告警; - 错误分布:403/429/503占比,429占比>30%说明限流策略失效;
- 缓存命中率:
cache_hits / total_requests,<70%需优化缓存策略。
告警通道:
- 企业微信机器人:发送简明摘要(“京东价格抓取成功率跌至82%,429错误占比65%”);
- 钉钉群:附带最近10次失败详情(URL、状态码、响应头);
- 邮件:每日汇总报告,含趋势图(Prometheus + Grafana)。
实操心得:某次告警显示429错误激增,我们以为是代理IP被封,排查后发现是目标站升级了Cloudflare,新增了cf-ray头校验。教训:监控必须包含响应头分析,不能只看状态码。
6. 从选型到落地:一个可复用的Agent检索模块模板
6.1 模块设计原则
- 单一职责:每个子模块只做一件事(采集、解析、校验、缓存);
- 可插拔:更换浏览器引擎(Playwright→Selenium)不改动业务逻辑;
- 可观测:每个环节输出结构化日志(trace_id、url、耗时、状态);
- 可降级:当Playwright失败时,自动回退到requests+bs4(牺牲准确性保可用性)。
6.2 核心代码结构
retriever/ ├── __init__.py # 暴露Retriever类 ├── base.py # AbstractRetriever基类 ├── static/ # 静态页采集器 │ ├── requests_loader.py │ └── scrapy_spider.py ├── dynamic/ # 动态页采集器 │ ├── playwright_loader.py │ └── puppeteer_loader.py ├── api/ # API采集器 │ └── httpx_client.py ├── parser/ # 解析器 │ ├── html_parser.py │ ├── pdf_parser.py │ └── json_parser.py ├── cache/ # 缓存层 │ ├── redis_cache.py │ └── memory_cache.py └── validator/ # 校验器 ├── price_validator.py └── date_validator.py6.3 关键实现片段
可降级采集器(retriever/base.py):
class FallbackRetriever: def __init__(self, primary_loader, fallback_loader): self.primary = primary_loader self.fallback = fallback_loader async def load(self, url): try: return await self.primary.load(url) except Exception as e: logger.warning(f"Primary loader failed for {url}: {e}") return await self.fallback.load(url) # 使用示例 retriever = FallbackRetriever( PlaywrightLoader(), RequestsLoader() )结构化日志(retriever/utils.py):
import structlog logger = structlog.get_logger() async def trace_retrieval(url, loader_name): start_time = time.time() try: result = await loader.load(url) duration = time.time() - start_time logger.info("retrieval_success", url=url, loader=loader_name, duration=round(duration, 2), content_length=len(result.text)) return result except Exception as e: duration = time.time() - start_time logger.error("retrieval_failed", url=url, loader=loader_name, duration=round(duration, 2), error=str(e)) raise缓存键生成(retriever/cache/redis_cache.py):
def generate_cache_key(url, context_hash=None): key_parts = [url] if context_hash: key_parts.append(context_hash) # 添加版本号,便于缓存批量失效 key_parts.append("v2") return hashlib.md5(":".join(key_parts).encode()).hexdigest()6.4 部署 checklist
- [ ] Playwright依赖安装:
playwright install chromium --with-deps(Ubuntu需额外apt-get install libglib2.0-0); - [ ] Redis连接池配置:
max_connections=100,避免连接耗尽; - [ ] 日志轮转:
structlog配置RotatingFileHandler,单文件≤100MB; - [ ] 环境隔离:开发/测试/生产使用不同Redis DB,避免缓存污染;
- [ ] 健康检查端点:
/health返回{"status": "ok", "cache_hit_rate": 0.87}。
我在实际项目中发现,最常被忽略的是Playwright的依赖安装。Docker镜像里只装playwright包,没装Chromium二进制,导致容器启动时报BrowserType.connect: Failed to connect。解决方案:在Dockerfile中明确安装:
RUN apt-get update && apt-get install -y \ libglib2.0-0 \ libnss3 \ libatk1.0-0 \ libatk-bridge2.0-0 \ libcups2 \ libxkbcommon-x11-0 \ libxcomposite1 \ libxdamage1 \ libxfixes3 \ libxrandr2 \ libgbm1 \ libpango-1.0-0 \ libcairo2 \ libatspi2.0-0 \ libxss1 \ libxtst6 \ libpci3 \ libdrm2 \ libgl1 \ libglib2.0-0 \ && rm -rf /var/lib/apt/lists/*这个模板已在3个客户项目中复用,平均节省检索模块开发时间40%。它不追求“最先进”,而是用最稳的组合,把互联网检索这个最不稳定的环节,变成Agent架构中最可靠的基石。