Skip to content
··阅读时间3分钟

我如何借助 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),而不是 WeaviateChroma。对我当前需求来说,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

继续阅读

我的旅程
联系
语言
偏好设置