深夜奶爸的AI Agent实战:从爬虫到Embedding的折腾之路

CloudRunner
2026-03-14 02:27
阅读 1681

上周五晚上十点半,娃终于睡了。我蹑手蹑脚地关上儿童房的门,像执行秘密任务一样溜回书房——这是属于我的“黄金两小时”。成都的夏夜闷热,窗外偶尔传来几声蛙鸣,空调嗡嗡作响,键盘敲击声成了深夜唯一的BGM。

作为一名在本地一家中型电商公司做前端的两个娃的奶爸,白天被需求、会议、联调、改稿轮番轰炸,只有晚上才能真正静下心来搞点技术探索。最近团队接了个新需求:给内部知识库加个智能问答功能,让运营和客服能快速查到商品规则、活动条款这些文档。产品经理甩过来一句:“能不能像Copilot那样,问啥答啥?”

我内心OS:你当AI是泡面啊,三分钟搞定?但嘴上还是笑着说:“可以试试。”

于是,一场关于AI Agent、Embedding 和爬虫的深夜折腾,就此开始。


问题比想象中复杂

我们现有的知识库散落在 Confluence、内部Wiki、甚至部分PDF和Excel里。最麻烦的是,有些关键信息居然藏在老系统的HTML页面里——对,就是那种十年前写的、连CSS都内联的“古董”页面。运维大哥说:“别动,一动就崩。”测试同学补刀:“上次谁改了静态页,导致双11前404了一天?”

行吧,那我只能“偷”数据了。

爬虫:不是不能用,是得低调用

首先想到的是写个爬虫把那些HTML页面抓下来。但直接用 requests + BeautifulSoup 太粗暴,容易被Nginx限流,而且页面结构乱得像我家娃搭的积木——毫无逻辑可言。

于是我用了 Playwright,模拟真实用户行为,带延时、带User-Agent轮换,还加了失败重试机制。关键代码长这样:

// playwright-crawler.ts
import { chromium } from 'playwright';

const crawlPage = async (url: string) => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();
  
  // 模拟人类行为:随机延迟 + 随机UA
  await page.setViewportSize({ width: 1366, height: 768 });
  await page.route('**/*', route => {
    if (route.request().resourceType() === 'image') return route.abort();
    route.continue();
  });

  try {
    await page.goto(url, { waitUntil: 'networkidle', timeout: 10000 });
    await page.waitForTimeout(1000 + Math.random() * 2000); // 随机停顿

    const content = await page.evaluate(() => {
      // 只取主内容区,避开导航/广告等噪音
      const main = document.querySelector('#main-content') || document.body;
      return main?.innerText || '';
    });

    return content.trim();
  } catch (err) {
    console.error(`Failed to crawl ${url}:`, err.message);
    return null;
  } finally {
    await browser.close();
  }
};

跑了一晚上,抓了300多页,去重后整理成纯文本。中间有两次被运维盯上,问我是不是在压测——还好我提前打了招呼,不然又得背锅。

小贴士:公司内网爬虫一定要和运维、安全团队沟通!别学我,差点被当成“内鬼”。


Embedding:让机器“看懂”文字

有了原始文本,下一步是怎么让AI理解它。这时候 Embedding 就派上用场了。

简单说,Embedding 就是把一段文字转换成一个高维向量(比如1536维),语义相近的句子,向量距离也近。比如“如何退货?”和“我想退掉刚买的鞋子”在向量空间里应该靠得很近。

我一开始想用 OpenAI 的 text-embedding-ada-002,但公司不让外传数据。于是转向开源方案:Sentence-BERT(简称 SBERT)。

在本地跑了个 all-MiniLM-L6-v2 模型(轻量级,适合我这台8G内存的MacBook Pro),用 Python 写了个小服务:

# embedding_service.py
from sentence_transformers import SentenceTransformer
import numpy as np

model = SentenceTransformer('all-MiniLM-L6-v2')

def embed(text: str) -> list[float]:
    vector = model.encode(text)
    return vector.tolist()  # 转成JSON可序列化的list

然后把每篇文档切块(chunk),每块300字左右,分别生成向量,存进 Pinecone(向量数据库)。为什么选 Pinecone?因为免费额度够我折腾,而且API简单,不像 Milvus 要自己搭集群——我哪有时间搞运维?

向量数据库 上手难度 免费额度 适合场景
Pinecone ⭐⭐ 10万向量 快速原型、小项目
Weaviate ⭐⭐⭐ 有限 需要图关系
Milvus ⭐⭐⭐⭐ 大规模生产

AI Agent:不只是聊天机器人

现在有了向量库,怎么回答用户问题?很多人以为就是“输入问题 → 查相似文档 → 丢给LLM生成答案”。但实际业务中,Agent 的决策能力更重要。

比如用户问:“618期间买的衣服能七天无理由退吗?”

理想流程应该是:

  1. 判断是否涉及“退货政策”
  2. 查找“618活动规则”和“通用退货条款”
  3. 如果两者冲突,优先活动规则
  4. 生成答案时注明依据来源

这就需要一个 Agent 来协调检索、判断、调用工具。我用 LangChain 搭了个简易 Agent,核心逻辑如下:

// agent.ts
import { RetrievalQAChain } from "langchain/chains";
import { OpenAIEmbeddings } from "langchain/embeddings/openai";
import { PineconeStore } from "langchain/vectorstores/pinecone";

// 注意:这里用的是本地部署的LLM(如Llama.cpp),避免数据外泄
const model = new ChatOllama({ model: "llama3" });

const vectorStore = await PineconeStore.fromExistingIndex(
  new OpenAIEmbeddings(), // 实际用的是本地SBERT服务封装的embeddings
  { pineconeIndex }
);

const chain = RetrievalQAChain.fromLLM(model, vectorStore.asRetriever({
  searchKwargs: { k: 3 } // 返回最相关的3段
}));

const response = await chain.call({ query: "618期间买的衣服能七天无理由退吗?" });
console.log(response.text);

但问题来了:本地 LLM 回答太飘!有一次它说“根据公司规定,所有商品均可终身退货”——我当场吓醒,赶紧加了 输出约束引用校验

最终方案是:Agent 输出必须包含 [来源: 文档ID],前端展示时可点击跳转原文。这样既保证可信度,也方便运营修正错误。


踩坑实录:那些让我想砸电脑的瞬间

  1. 中文分块灾难
    一开始用英文的 RecursiveCharacterTextSplitter,结果中文被切成“今天天气很/好”,语义全废。后来换成按句号+分段符切,才解决。

  2. Embedding 版本不一致
    本地训练用的 SBERT v2,线上服务用了 v3,向量维度不同,检索结果全是乱的。花了两小时才定位到是模型版本问题。

  3. 爬虫被反爬
    有个页面加了 Cloudflare,Playwright 直接返回503。最后用 puppeteer-extra + stealth-plugin 才绕过,但稳定性差,改成人工导出CSV更靠谱。

  4. 娃半夜哭醒
    最致命的一次:凌晨1点,Agent 终于跑通,我正准备提交代码,大宝突然发烧,折腾到四点……第二天顶着黑眼圈开会,被产品经理问:“你这眼袋是用Three.js做的吧?”


效果与反思

上线两周,内部使用率超预期。客服反馈:“以前查个规则要翻半小时,现在5秒出答案。”更惊喜的是,运营开始主动往知识库补内容——因为知道会被AI用到。

但我也清醒:这玩意儿离“Copilot”还差十万八千里。它只能回答已有文档里的问题,无法推理、无法联网、更不能处理模糊需求。比如用户问“帮我找个性价比高的防晒霜”,它就懵了。

不过,作为一个下班后才有时间学习的奶爸程序员,能用周末+深夜的碎片时间,把 AI Agent、Embedding、爬虫串起来解决实际问题,已经很满足了。技术不是炫技,而是让工作少一点重复,多一点创造


给同行的建议

如果你也在中小厂,想搞类似的东西,我的经验是:

  • 先小范围验证:别一上来就搞大模型,先用规则+关键词匹配,再逐步引入AI。
  • 数据质量 > 模型先进:垃圾进,垃圾出。花80%时间清洗和结构化数据。
  • 安全第一:公司数据别乱传第三方API,本地部署虽麻烦但安心。
  • 留后路:所有AI回答必须可追溯、可关闭。别让算法背锅。

最后,感谢我家那位每天帮我带娃到十点,才让我有这点“奢侈”的编码时间。技术人的浪漫,大概就是深夜书房里,一行行跑通的代码,和窗外成都温柔的夜色。

下次更新?可能得等娃上幼儿园了 😅


附:技术栈清单

  • 爬虫:Playwright + TypeScript
  • Embedding:Sentence-BERT (all-MiniLM-L6-v2)
  • 向量库:Pinecone(免费版)
  • LLM:Llama3 8B via Ollama(本地运行)
  • Agent框架:LangChain
  • 部署:Docker + 内网FastAPI服务

注:全文约3520字,纯手打,无AI代笔——毕竟我家AI还在吃奶呢。

评论 0

最热最新
暂无评论
CloudRunnerLv.1
0
影响力
0
文章
0
粉丝