深夜奶爸的AI Agent实战:从爬虫到Embedding的折腾之路
上周五晚上十点半,娃终于睡了。我蹑手蹑脚地关上儿童房的门,像执行秘密任务一样溜回书房——这是属于我的“黄金两小时”。成都的夏夜闷热,窗外偶尔传来几声蛙鸣,空调嗡嗡作响,键盘敲击声成了深夜唯一的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期间买的衣服能七天无理由退吗?”
理想流程应该是:
- 判断是否涉及“退货政策”
- 查找“618活动规则”和“通用退货条款”
- 如果两者冲突,优先活动规则
- 生成答案时注明依据来源
这就需要一个 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],前端展示时可点击跳转原文。这样既保证可信度,也方便运营修正错误。
踩坑实录:那些让我想砸电脑的瞬间
中文分块灾难
一开始用英文的RecursiveCharacterTextSplitter,结果中文被切成“今天天气很/好”,语义全废。后来换成按句号+分段符切,才解决。Embedding 版本不一致
本地训练用的 SBERT v2,线上服务用了 v3,向量维度不同,检索结果全是乱的。花了两小时才定位到是模型版本问题。爬虫被反爬
有个页面加了Cloudflare,Playwright 直接返回503。最后用puppeteer-extra+stealth-plugin才绕过,但稳定性差,改成人工导出CSV更靠谱。娃半夜哭醒
最致命的一次:凌晨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