我如何借助 AI Agent 从“编码流沙”中爬出来
我在几乎零编程基础和满腔热情下做了 chatbot v0.1,结果变成 CSV 数据库 + 原始分块的灾难组合,直到借助 AI agents 才逐步走出泥潭。
更新(2026): 这个 chatbot 后来演进成了 Sydney!经历多轮迭代后,Sydney 现在在 /ask/,并聚焦博客内容与产品。
去年 11 月初,我发布了自己动手做的 chatbot v0.1。当时我写的是:“作为 coding 新手,v0.1 是第一步,但 有明显局限。”后来证明,这句话其实还说轻了。我当时没有真正意识到整套流程和实现有多笨重。现实是:第一次尝试虽然真诚,但更像是“热情驱动 + 技能不足”拼凑出的原型。
这篇不只是 follow-up,而是对那之后整段过程的深挖——其中有反复试错,也有非常宝贵的教训。我会把这段经历的技术细节都摊开讲,不只是为了透明,也希望对走类似路径的人有一点参考。(补充背景:我是中年广告从业者,之前几乎没有 coding 经验。)
如上所述,为了做出 v0.1,我主要照着这门短课 “Building Systems with the ChatGPT API” 和 OpenAI 两本 cookbook: Question answering using embeddings-based search 与 How to count tokens with tiktoken。
下面是我为什么说 v0.1 很糟:
- Chunking:我只是按 token 长度把长文硬切成小块,也就是最原始的静态字符分块。几乎是最 primitive 的切法 :D 如果你想理解为什么这很糟,推荐看 Greg Kamradt 关于 5 levels of text splitting 的说明。
- Embedding:embedding 上我踩了很多坑。虽然用了 OpenAI embedding 模型,但一直撞 API 限速,导致流程中途失败。后来才学会 batch 请求并在 batch 间加 timeout 以规避限制。最后我把 embeddings 存到一个 .csv 文件里,充当“临时数据库”。
- Database:我知道 CSV 不是理想数据库,但当时我没有能力切更优方案。
- Metadata:我起初没意识到 publish date、post URL 这类 metadata 对问答准确性很关键。后来不得不重新 embedding 和保存,把 metadata 补进去。
- Retriever:我当时不知道 retriever 还有不同类型和算法,只是直接用 OpenAI relevance search 拉一个硬编码结果数量。
- Memory:要支持多轮对话,chatbot 必须记住用户之前说过什么。这里在当时 gpt-3.5 的上下文限制下,出现了明显 trade off:chunk size 与检索返回条数之间互相掣肘。
- 比如 chunk size 设 800 tokens,retriever 返回 top 8,光检索内容就 6,400 tokens,超过旧模型上下文上限的 50%。
- 上面只是 1 个问题,你可以想象多轮对话下 memory 会多快被塞满。
- 解决之一是减小 chunk、减少返回条数,但在基础 retriever 场景下,这通常意味着上下文不够,回答变浅。
- 我当时甚至 没有用 IDE。所有代码都用 Mac 的 TextEdit 改 :D(我是不是说过我很菜了? :P)
- 还可以继续讲很多,但你应该已经懂那个画面了。
我的“死亡谷”
为了突破 v0.1 的局限,我接着上了几门在线课,希望补齐关键短板。但进展并不顺,很多时候都走进死胡同。
我尝试了向量数据库练习(Vector Databases: from Embeddings to Applications with Weaviate)、评估型 RAG(Building and Evaluating Advanced RAG Applications with Llama-Index + Truera)、高级检索(Advanced Retrieval for AI with Chroma)。我怎么试都很难把这些理论真正落到自己的博客数据上。
是课程不好吗?不是。主要短板是我自己的底层能力还不够。但连续失败真的非常挫败,甚至有点打击人。我一度感觉自己掉进了“谷底”,不知道下一步怎么走。
逐步累积的小胜利
不过,反复失败里也慢慢长出了一些有价值的东西:
-
从 TextEdit 迁移到 VS Code
-
开始用 GitHub Copilot 扩展
-
理解并接受 Jupyter Notebook 作为开发环境
最后那门课程里提到 LangChain(一个很流行的新聊天机器人框架)。其实几个月前我就尝试过 LangChain 课程(“LangChain: Chat with Your Data” 和 “Functions, Tools and Agents with LangChain”),但当时基本没成功。现在带着前面硬啃出来的经验,重看它的 docs 时,很多概念终于 click 了。它的模块化结构也开始变得直观。
我开始能想象如何把 LangChain 的能力嫁接到自己的项目。终于,看到了路径!我一点点理清它在数据导入、embedding、存储和检索上的流水线。
每做成一块,信心就回一点。V2 也开始成形……
重建地基
以 Langchain 为引导,我决定把 chatbot 从地基开始重做:
把 WordPress 导出数据转进 JSON
调了几轮参数后,LangChain 的 JSONLoader 终于稳定解析我的导出文章。输入数据得到验证后,后续流水线就能正常运转。(这步代码在公开 Github repo 的 “DataIngestionAndIndexing.ipynb” 文件里。)
自动化文本切分
我之前那种朴素 token 长度切块,被 LangChain 的 SentenceTransformers 替代。它用更合理 NLP 方式切语义单元,不再把句子从中间截断。配置也能把 chunk 控在适配模型 memory 的范围。
from langchain.text_splitter import SentenceTransformersTokenTextSplitter
# Define the token splitter with specific configurations
token_splitter = SentenceTransformersTokenTextSplitter(
chunk_overlap=0, # Overlap between chunks
tokens_per_chunk=256 # Number of tokens per chunk
)
# Split the documents into chunks based on tokens
all_splits = token_splitter.split_documents(documents)
print(f"Total document splits: {len(all_splits)}")
生成 embeddings 与索引
过去我在 OpenAI API 限速上的痛点,借助 LangChain 的 OpenAI embeddings 封装基本消失。只要两行代码就能把文本特征提出来。
向量库方面,我最后选了 FAISS(Facebook AI Similarity Search),而不是 Weaviate 或 Chroma。对我当前需求来说,FAISS 在能力与复杂度之间平衡更合适。它的 CPU 版本可以很快建立博客片段索引,输出一个紧凑可检索数据库。我再也不需要担心 batch 和 OpenAI API 限速问题。
Langchain 支持很多向量库,列表在 here。
# Initialize embeddings and FAISS vector store
embeddings = OpenAIEmbeddings()
db = FAISS.from_documents(all_splits, embeddings)
# Save the vector store locally
db.save_local("path/to/save/faiss_index") # Placeholder for save path/ index name
就两行!就是这么短。
我还注意到,如果你的内容不小(我有 400+ 篇文章),embedding 后不要立刻用 retriever 开跑,索引需要一点时间完成/稳定。
评估 retrievers
这个 retriever 用 FAISS 时只要 1 行代码就能搭起来 :D
retriever = db.as_retriever(search_type="mmr")
当索引和 retriever 都就位后,数据流水线已经具备支撑智能 chatbot 的条件了!
架构化 Conversational Agent
我决定用 langchain 的 agent framework 来构建这个 chatbot。现在看来是不是有点“超前配置”?是的。但我的想法是后续逐步给它加更多“tools”(功能)。Langchain 在这块上手非常快。
embeddings = OpenAIEmbeddings()
db = FAISS.load_local("path/to/your/faiss_index_file", embeddings) # Replace the path with your actual FAISS index file path
retriever = db.as_retriever(search_type="mmr")
tool = create_retriever_tool(
retriever,
"search_your_blog", # Replace "search_your_blog" with a descriptive name for your tool
"Your tool description here" # Provide a brief description of what your tool does
)
tools = [tool]
prompt_template = ChatPromptTemplate.from_messages([
# Customize the prompt template according to your chatbot's persona and requirements
])
llm = ChatOpenAI(model_name="gpt-3.5-turbo-1106", temperature=0)
llm_with_tools = llm.bind_tools(tools)
agent = (
{
"input": lambda x: x["input"],
"agent_scratchpad": lambda x: format_to_openai_tool_messages(x["intermediate_steps"]),
"chat_history": lambda x: x["chat_history"],
}
| prompt_template
| llm_with_tools
| OpenAIToolsAgentOutputParser()
)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
最终完整 agent Python 代码
最后,如果你想直接试 chatbot v2,入口在 here。
“奇怪,chatbot 竟然不认识你 Chandler?”
P.S:谢谢一些读者来提醒我 chatbot 完全不知道我是谁。你们说得对!原因是我只导出了“已发布文章”,却忘了导出 “About” 页面。这是我第二次犯同样错误,所以我已经把“关于我”的问题加入 eval 问题集。长记性了!
感谢大家持续给建设性反馈。欢迎继续砸过来。还有,是的,我知道 chatbot 首次启动仍然很慢,我也在修。:|(我是不是说过我很菜了? :P)
快速更新
“chatbot 不认识我”这个问题已修复。具体我做了这些:
- 从 WordPress 按上文流程导出 “About me” 页面到 .XML。
- 按前述方式做文本切分和 FAISS embedding,并以新名称保存本地向量库做 retriever 测试。
# save the vector store to local machine
db.save_local("faiss_index_about")
# set up the retriever to test the new vector store about Chandler
from langchain.retrievers.multi_query import MultiQueryRetriever
llm = ChatOpenAI(temperature=0)
retriever_from_llm = MultiQueryRetriever.from_llm(
retriever=db.as_retriever(), llm=llm
)
# test retriever
question = "Who is Chandler Nguyen?"
results = retriever_from_llm.get_relevant_documents(query=question, top_k=8)
for doc in results:
print(f"Content: {doc.page_content}, Metadata: {doc.metadata}")
- 结果发现,按文档 here 合并两个 FAISS 向量库其实非常简单。
# Try to merge two FAISS vector stores into 1
# load the vector store from local machine
from langchain_community.vectorstores import FAISS
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
embeddings = OpenAIEmbeddings()
db_1 = FAISS.load_local("faiss_index_about", embeddings)
db_2 = FAISS.load_local("faiss_index", embeddings)
db_2.merge_from(db_1)
# save the vector store to local machine
db_2.save_local("faiss_index_v2")
# test the new vector store to confirm correct retrieved documents
retriever = db_2.as_retriever(search_type="mmr")
results = retriever.get_relevant_documents("Who is Chandler Nguyen?")
for doc in results:
print(f"Content: {doc.page_content}, Metadata: {doc.metadata}")
- 后面流程就和前面一致,改用新向量库即可。
2 月 14 日更新:Chatbot v2.10 发布
在这次部署两周后,我发布了 v2.10,重点是更快速度、更好扩展性与更简体验。详细见 here。
3 月 25 日更新:从前端升级到 Docker 挣扎与突破
详细见 here。





