LangChain项目工程化:模块拆分、测试驱动开发(TDD)与CI/CD集成实战

问题背景

在LangChain系列前7篇文章中,我们逐步完成了从基础应用到高级功能的开发。随着项目规模扩大(例如支持多模态输入、多Agent协作),原有单体架构的局限性逐渐暴露:

  1. 代码耦合度高:早期版本将文档加载、向量化、Chain编排等逻辑堆积在单一脚本中,导致修改RetrievalQA的检索逻辑时,意外破坏了AgentExecutor的工具调用功能。
  2. 测试覆盖不足:缺乏自动化测试,重构时出现文档分割粒度变化导致向量检索结果不一致的回归问题。
  3. 部署流程低效:手动部署需要同步依赖版本、更新环境变量,且无法快速回滚至稳定版本。

本文以一个企业级RAG系统为例,通过模块拆分测试驱动开发(TDD)CI/CD集成,实现可维护、可扩展的LangChain工程化实践。


原理分析

1. 模块拆分原则

LangChain应用的核心逻辑可分为以下模块:

  • 数据层:负责文档加载、清洗与向量化(DocumentLoaderTextSplitterVectorStore)。
  • 业务层:定义Chain/Agent的编排逻辑(RetrievalQAAgentExecutor)。
  • 接口层:提供API或CLI交互(FastAPI、Gradio)。

拆分目标

  • 单一职责:每个模块仅处理一类任务,例如data/vector_store.py仅包含向量存储相关逻辑。
  • 依赖倒置:高层模块依赖抽象接口而非具体实现,例如service/rag_chain.py依赖DataProcessor接口而非Chroma类。

2. TDD(测试驱动开发)流程

TDD的核心是先写测试,再写代码,确保功能可验证。典型流程:

  1. Red:编写失败的测试用例,明确预期行为。
  2. Green:编写最小代码使测试通过,不引入额外逻辑。
  3. Refactor:重构代码保持测试通过,优化结构。

LangChain适配

  • 使用pytest测试Chain/Agent的输出是否符合预期。
  • 模拟LLM响应以减少外部依赖(例如unittest.mock)。

3. CI/CD集成

通过GitHub Actions实现自动化流程:

  1. 代码提交触发测试:运行单元测试和集成测试。
  2. 测试通过后构建Docker镜像:确保环境一致性。
  3. 自动部署到测试环境:使用Kubernetes滚动更新,支持快速回滚。

落地步骤

步骤1:模块化重构

目录结构

langchain_project/  
├── src/  
│   ├── __init__.py  
│   ├── data/          # 数据层  
│   │   ├── __init__.py  
│   │   ├── loader.py  
│   │   ├── splitter.py  
│   │   └── vector_store.py  
│   ├── service/       # 业务层  
│   │   ├── __init__.py  
│   │   ├── rag_chain.py  
│   │   └── agent.py  
│   └── api/           # 接口层  
│       ├── __init__.py  
│       └── app.py  
├── tests/             # 测试用例  
│   ├── __init__.py  
│   ├── test_data.py  
│   ├── test_service.py  
│   └── test_api.py  
├── requirements.txt  
├── Dockerfile  
└── .github/workflows/ci.yml  

代码示例(src/data/loader.py

from langchain.document_loaders import PyPDFLoader  
from typing import List  

class DocumentLoader:  
    def __init__(self, file_path: str):  
        self.file_path = file_path  

    def load(self) -> List[str]:  
        """加载PDF文档并返回文本列表"""  
        loader = PyPDFLoader(self.file_path)  
        documents = loader.load()  
        return [doc.page_content for doc in documents]  

步骤2:TDD实践

测试代码(tests/test_data.py

import pytest  
from src.data.loader import DocumentLoader  

def test_load_pdf(tmp_path):  
    """测试PDF加载功能边界条件:空文件处理"""  
    # 准备测试文件  
    pdf_path = tmp_path / "empty.pdf"  
    pdf_path.write_text("")  # 模拟空文件  

    # 执行测试  
    loader = DocumentLoader(str(pdf_path))  
    content = loader.load()  
    assert len(content) == 0  # 验证空文件是否返回空列表  

def test_load_pdf_normal(tmp_path):  
    """测试正常PDF加载"""  
    pdf_path = tmp_path / "test.pdf"  
    pdf_path.write_text("Mock PDF content")  

    loader = DocumentLoader(str(pdf_path))  
    content = loader.load()  
    assert len(content) == 1  
    assert "Mock PDF content" in content[0]  

运行测试

pytest tests/test_data.py -v  

步骤3:CI/CD配置

配置文件(.github/workflows/ci.yml

name: CI/CD Pipeline  
on: [push, pull_request]  

jobs:  
  test:  
    runs-on: ubuntu-latest  
    steps:  
      - uses: actions/checkout@v4  
      - name: Set up Python  
        uses: actions/setup-python@v4  
        with:  
          python-version: "3.11"  
      - name: Install dependencies  
        run: pip install -r requirements.txt  
      - name: Run tests  
        run: pytest tests/ --cov=src  

  build:  
    needs: test  
    runs-on: ubuntu-latest  
    if: github.ref == 'refs/heads/main'  
    steps:  
      - name: Build Docker image  
        run: docker build -t langchain-app:latest .  
      - name: Deploy to staging  
        run: |  
          kubectl set image deployment/staging langchain-app=langchain-app:latest  
          kubectl rollout status deployment/staging  

常见坑与排查

坑1:模块间循环依赖

现象service/rag_chain.py依赖data/vector_store.py,而data/vector_store.py又引用service/rag_chain.py中的工具类。

排查路径

  1. 使用pydeps工具检测循环依赖:
    pydeps src --max-bacon=2 --show-deps  
    
  2. 检查模块导入路径,例如在data/vector_store.py中避免直接导入rag_chain

解决:引入抽象层,例如定义DataProcessor接口:

from abc import ABC, abstractmethod  

class DataProcessor(ABC):  
    @abstractmethod  
    def process(self, data: str) -> str:  
        pass  

坑2:测试因外部API失败

现象:集成测试因网络问题或LLM服务不可通过失败。

排查日志

tests/test_service.py::test_rag_chain - FAILED  
    def test_rag_chain():  
        chain = get_rag_chain()  
>       result = chain.run("What is LangChain?")  
E       requests.exceptions.ConnectionError: ...  

解决:使用unittest.mock模拟LLM响应:

from unittest.mock import patch  

@patch('src.service.rag_chain.LLM')  
def test_rag_chain(mock_llm):  
    mock_llm.return_value.run.return_value = "LangChain is a framework for LLM applications."  
    chain = get_rag_chain()  
    result = chain.run("What is LangChain?")  
    assert "LangChain is a framework" in result  

坑3:CI/CD部署失败

现象:Docker镜像构建失败,提示ModuleNotFoundError: No module named 'langchain'

排查路径

  1. 检查requirements.txt是否包含langchain依赖。
  2. 验证Dockerfile中的pip install命令是否正确执行。

解决:优化Dockerfile,使用多阶段构建减少镜像体积:

FROM python:3.11-slim as builder  
WORKDIR /app  
COPY requirements.txt .  
RUN pip install --no-cache-dir -r requirements.txt  

FROM python:3.11-slim  
WORKDIR /app  
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages  
COPY src/ ./src/  
COPY app.py .  
CMD ["python", "app.py"]  

总结

本文通过模块拆分、TDD和CI/CD,将LangChain应用从原型升级为工程化项目。关键点总结:

  1. 模块化是大型项目的基础,需明确职责边界。例如将DocumentLoaderVectorStore分离,便于替换向量数据库(如从Chroma切换到FAISS)。
  2. TDD能显著提升代码质量,但需平衡测试粒度。单元测试应覆盖边界条件(如空文件输入),而集成测试需模拟外部依赖。
  3. CI/CD自动化流程可减少人为错误,建议结合监控(如Prometheus)和日志分析(如ELK)。

性能权衡:在模块拆分时,过度抽象会增加调用链路长度,影响性能。例如service/rag_chain.py通过接口调用data/vector_store.py,会增加约5%的响应时间,但提升了可维护性。

失败回滚:在CI/CD流程中,部署到测试环境后应添加健康检查:

- name: Health check  
  run: |  
    until curl -f http://staging.health-check/ready; do  
      sleep 5  
    done  

若健康检查失败,自动回滚到上一个版本:

kubectl rollout undo deployment/staging  

下一步可探索:

  • 使用pydantic验证数据结构,确保输入输出的类型安全。
  • 集成Sentry进行错误追踪,快速定位生产环境问题。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐