目录

一个专注音视频领域的小圈子

前面九篇已经把第一阶段的内容基本走完了:

  1. 环境准备和模型调用。
  2. PromptTemplate 和结构化输出。
  3. 工具调用和 Agent。
  4. 本地 Markdown RAG。
  5. 向量检索 RAG。
  6. 持久化索引。
  7. 检索质量调优。
  8. 带工具的博客问答 Agent。
  9. 多轮对话和上下文记忆。

这一篇做一个收束:把本地博客问答封装成一个简单 CLI。

本文对应代码位于 code/langchain-demo/chapter10

本篇目标

第十篇不再引入新概念,而是做一个能直接使用的命令行入口:

uv run python -m chapter10.app search "FFmpeg 硬解码" --category ffmpeg
uv run python -m chapter10.app ask "Python 函数式编程讲了什么?"

其中:

  • search:只检索文章,不调用模型。
  • ask:先检索文章,再调用模型回答。

这样平时调试时,可以先用 search 看检索结果,再用 ask 看最终回答。

代码结构

第十篇代码结构如下:

chapter10/
  __init__.py
  config.py
  llm.py
  rag_utils.py
  app.py

其中:

  • rag_utils.py:读取、切分、检索、格式化上下文。
  • app.py:命令行入口。

这一章的目标是把前面几篇的能力整理成一个更像工具的入口。

argparse 子命令

CLI 使用 Python 标准库 argparse

核心代码如下:

def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Local blog QA CLI")
    subparsers = parser.add_subparsers(dest="command")

    ask_parser = subparsers.add_parser("ask", help="retrieve context and ask LLM")
    ask_parser.add_argument("question", nargs="?", default=DEFAULT_QUESTION)
    ask_parser.add_argument("--top-k", type=int, default=4)
    ask_parser.add_argument("--category")

    search_parser = subparsers.add_parser("search", help="only show retrieved posts")
    search_parser.add_argument("question", nargs="?", default=DEFAULT_QUESTION)
    search_parser.add_argument("--top-k", type=int, default=4)
    search_parser.add_argument("--category")

这里把 asksearch 分成两个子命令。

它们共享几个参数:

  1. question:用户问题。
  2. --top-k:返回多少个结果。
  3. --category:限定博客分类。

如果不传子命令,默认执行 ask

search 命令

search 不调用模型,只展示检索结果:

if args.command == "search":
    results = retrieve(args.question, chunks, args.top_k, args.category)
    print_search_results(results)
    return

运行:

uv run python -m chapter10.app search "FFmpeg 硬解码" --category ffmpeg

输出类似:

Sources:
1. FFmpeg 调用 MediaCodec 硬解码到 Surface 上
   blog/content/post/ffmpeg/FFmpeg 调用 MediaCodec 硬解码到 Surface 上/index.md
   ...
2. FFmpeg 调用 Android MediaCodec 进行硬解码(附源码)
   ...

这个命令很适合调试。

如果 search 的结果都不相关,那就不用急着看 ask 的回答,应该先调整检索参数。

ask 命令

ask 会先检索,再调用模型:

results = retrieve(args.question, chunks, args.top_k, args.category)
answer = answer_question(args.question, results)

模型调用逻辑还是熟悉的 LCEL:

chain = prompt | create_chat_model(temperature=0.2) | StrOutputParser()
return chain.invoke(
    {
        "question": question,
        "context": format_context(chunks),
    }
)

运行:

uv run python -m chapter10.app ask "Python 函数式编程讲了什么?"

输出会先列出检索来源,再给出回答。

这种输出顺序很实用,因为可以先判断模型回答是不是基于正确资料。

为什么要先做 CLI

很多 AI 应用一开始就想做 Web 页面。

但在学习阶段,我更建议先做 CLI。

原因很简单:

  1. CLI 更容易调试。
  2. 不需要处理前端状态。
  3. 输入输出清楚。
  4. 很适合验证 RAG、Agent、Memory 这些核心链路。

等 CLI 跑顺了,再把它封装成 Web API 或页面,会更稳。

第一阶段小结

到第十篇为止,第一阶段已经完成。

这一阶段的目标不是做一个大而全的应用,而是把 LangChain 常用的基础能力都实践一遍:

LLM 调用
PromptTemplate
结构化输出
Tool Calling
Agent
Document
Text Splitter
Retriever
RAG
Memory
CLI 封装

后面如果继续写第二阶段,就可以往更接近真实项目的方向走:

  1. 接入真正的 embedding 模型。
  2. 使用 Chroma、FAISS 或 PGVector。
  3. 做增量索引。
  4. 做 Web API。
  5. 加入评测集和自动化评估。
  6. 接入 LangSmith 或其他 tracing 工具。

小结

这一篇完成了第一阶段最后一个 demo:本地知识库问答 CLI。

它不是一个复杂应用,但已经能把前面几篇的内容串起来。

从这里开始,继续扩展就很自然了:先把 CLI 打磨稳定,再接向量数据库、Web 服务、评估和部署。

参考

  • code/langchain-demo/chapter10
  • Python argparse
  • LangChain ChatPromptTemplate
  • LangChain StrOutputParser

原创文章,转载请注明来源:    LangChain 入门学习第十篇-封装一个本地知识库问答 CLI