本报告旨在为具备编程背景的开发者提供一份详尽的、专家级的技术指南。我们将从零开始,构建一个专门为小红书平台定制的 AI 内容创作代理(Agent),聚焦于高热度的“宠物”赛道。此代理的核心技术栈将采用 LangGraph 框架,并搭载一个完全离线运行的检索增强生成(RAG)知识库,确保数据隐私与独立运行能力。
报告将深入剖-析 LangGraph 的核心理念,阐明其相较于传统线性链式(Chain)结构的根本优势,并提供一个完整的、可运行的代码示例。通过本报告,您不仅将掌握如何构建一个复杂的 AI 代理,还将深刻理解 stateful(有状态)和 cyclical(循环)工作流在智能代理工程中的重要性。
第一部分:LangGraph 范式:构建有状态的 AI 代理工程
在深入实践之前,必须首先建立对 LangGraph 核心思想的深刻理解。这不仅是技术选型的问题,更关乎一种从“工作流”到“智能体”的设计哲学转变。本章节将系统性地阐述 LangGraph 的设计初衷、核心组件及其相较于传统 LangChain 链的革命性优势,为您后续的开发工作奠定坚实的理论基础。
1.1 超越线性链:有状态、循环推理的必要性
传统的语言模型应用,特别是那些基于 LangChain 表达语言(LCEL)构建的应用,其本质上是有向无环图(Directed Acyclic Graph, DAG) 1。这意味着数据流是单向的、线性的,从输入开始,经过一系列固定的处理步骤,最终到达输出。这种模式对于许多任务,如格式转换、简单问答等,是高效且直观的 3。
然而,一个真正意义上的“智能代理”,尤其是在执行如内容创作、研究分析或复杂规划等任务时,其“思考”过程极少是线性的。一个人类创作者在撰写小红书文案时,其工作流通常包含以下循环:
构思与研究:确定主题,搜集资料。
起草初稿:根据资料撰写第一版文案。
审阅与反思:评估初稿的质量,检查其是否符合平台风格、是否足够吸引人。
修改与润色:根据反思结果,返回修改初稿,可能多次循环这个过程。
定稿与发布:直到满意为止。
这个过程包含了循环(cycles)和状态(state)。代理需要“记住”它已经写了什么(状态),并根据评估结果决定是继续下一步还是“返回”上一步进行修改(循环)。传统的线性链无法优雅地实现这种自我修正和迭代优化的能力 4。
**无状态(stateless)**系统将每一次交互都视为独立的事件,无法保留上下文记忆 6。对于一个内容创作代理而言,这意味着它无法基于前一版草稿进行改进,也无法记住用户的修改意见。这种限制使得应用无法适应持续变化的上下文,最终导致输出质量低下,无法完成复杂的、需要迭代完善的任务 5。LangGraph 的出现,正是为了解决这一根本性问题,为构建能够进行复杂、循环推理的有状态应用提供了强大的工程框架 1。
1.2 LangGraph 揭秘:一个为生产环境设计的可控代理框架
LangGraph 是一个用于构建有状态、多角色(multi-actor)LLM 应用的库 1。它并非要取代 LangChain,而是作为其强大的扩展,专门用于为应用添加
循环能力 2。许多开发者可能会联想到“思维图”(Graph of Thoughts, GoT)等高级推理概念,LangGraph 正是实现这些复杂、非线性工作流的
底层使能技术。它提供了将这些抽象思考模式工程化的具体工具:图、节点、状态和条件边 7。
LangGraph 的设计理念反映了 AI 应用开发从原型到生产的演进趋势。在原型阶段,LangChain 的高层抽象(如 AgentExecutor)能帮助开发者快速启动和验证想法。然而,当应用走向生产环境时,这些高层抽象可能变得像“黑箱”,难以调试、扩展和精确控制,导致性能和可靠性问题 11。许多经验丰富的开发者发现,LangGraph 提供了一种更为“精炼”和“有序”的开发模式 2。它强制开发者明确地定义每一个工作步骤和决策逻辑,从而带来了无与伦比的
可控性和可观测性。这种确定性对于构建在多变生产环境中稳定运行的可靠代理至关重要 11。
这种从 LangChain 链到 LangGraph 图的转变,不仅仅是技术上的升级,更是一种思维模式的跃迁。它标志着开发者从简单的“提示链工程师”转变为真正的“代理架构师”。其核心是从构建线性的“管道”模型,转向设计稳健的、可观测的、可控的状态机(State Machine) 2。状态机是软件工程中一个成熟而强大的范式,对于程序员来说非常亲切。这种范式不把代理看作一个被调用的简单函数,而是看作一个拥有内部状态、并随时间演化的系统。正是这种有状态的本质,才使得记忆 7、人机协作 14 和迭代优化 5 等高级代理行为成为可能。因此,LangGraph 的流行,并不仅仅因为它能实现循环,而是因为它为构建行为类似自主、推理实体的应用提供了正确的架构原语。这种能力已被 LinkedIn、Uber、Replit 等公司在生产环境中验证 11。
1.3 LangGraph 代理的解剖学:状态、节点与边
一个 LangGraph 代理由三个核心组件构成:状态(State)、节点(Nodes)和边(Edges)。
状态 (StateGraph):代理的记忆中枢
StateGraph 是 LangGraph 的核心类,它定义了代理的“记忆”结构 9。这个“状态”通常通过 Python 的
TypedDict 来定义,它是一个中心化的数据对象,在图的每次执行步骤中被传递给每一个节点 1。这个状态对象代表了代理在任何时刻的“世界观”或“工作记忆”,是构建能够跨越多步交互并维持上下文的应用的基石 5。
节点对状态的更新有两种方式 9:
完全覆盖(Overwrite):节点返回一个新值,该值将完全替换状态中对应的键值。这适用于需要更新某个确定性结果的场景。
追加更新(Append):这是更有力的方式。节点返回的值会被追加到状态中某个已有列表的末尾。这对于记录历史消息、累积操作步骤或收集多轮反馈至关重要。通过在
TypedDict中使用Annotated和operator.add,可以轻松实现这一点。
节点(Nodes):代理的行动单元
节点是图中的基本工作单元,是真正的“执行者” 9。在 LangGraph 中,任何一个 Python 函数或 LangChain Runnable 都可以被定义为一个节点 1。每个节点接收当前的状态对象作为输入,并返回一个字典,该字典包含了对状态的更新 9。
这种模块化的设计极具优势,它允许我们将一个复杂的任务分解为一系列单一职责的“专家”节点。例如,我们可以创建一个专门负责调用工具的节点、一个负责调用 LLM 进行创作的节点,以及一个负责评估内容的节点 13。这种分而治之的策略使得工作流更加清晰,也更易于测试和维护。
边(Edges):代理的控制流
边定义了执行流程如何在节点之间流转,是代理的“神经系统” 4。LangGraph 提供了几种定义边的机制:
入口边(Entry Point):通过
set_entry_point("node_name")来指定图的起始节点 9。普通边(Normal Edges):通过
add_edge("source_node", "destination_node")来定义无条件的、确定的执行路径。例如,在调用工具后,总是返回给 LLM 节点进行下一步决策 9。条件边(Conditional Edges):这是 LangGraph 的“大脑”,也是实现智能行为的关键。通过
add_conditional_edge()定义,它允许图根据一个“路由”节点的输出,动态地决定接下来应该走向哪个节点 1。
条件边的工作机制如下:
指定起始节点:该节点的输出将被用作决策依据。
提供一个条件函数:这个函数接收起始节点的输出,并返回一个字符串,该字符串代表了接下来要走的路径的名称。
提供一个路径映射:这是一个字典,它的键是条件函数可能返回的字符串,值是对应的下游节点的名称。
例如,一个路由节点(如“创意总监”)在审阅草稿后,可以输出 “APPROVE” 或 “REVISE”。条件边会根据这个输出,将流程导向“最终定稿”节点或导回“草稿撰写”节点。正是这种机制,赋予了 LangGraph 创建循环、实现决策、并构建能够自我修正的智能代理的强大能力 9。
第二部分:构建离线知识核心:宠物赛道的 RAG 管道
要让我们的 AI 代理成为一个“宠物专家”,它需要一个强大、可靠且专业的知识库。本章节将详细指导您如何构建一个完全离线的检索增强生成(RAG)管道。我们将采用纯本地化的方案,确保数据的绝对隐私,杜绝任何 API 依赖和费用,并完全掌控知识库的内容和更新。
2.1 本地化 RAG 的理论依据
选择构建一个本地化的 RAG 管道,主要基于以下几个核心优势 18:
数据隐私与安全:所有数据,无论是知识库内容还是用户交互,都保留在本地机器上,永远不会传输到外部服务器。这对于处理敏感信息或保护创作内容至关重要。
成本效益:无需为第三方 LLM API 或 Embedding API 支付任何费用。一次性构建后,可以无限次使用,极大地降低了长期运营成本。
性能与稳定性:不受网络延迟或第三方服务中断的影响。本地化部署提供了稳定、可预测的响应时间。
完全控制与定制:您可以完全控制知识库的内容、更新频率,并自由选择和切换本地运行的 Embedding 模型和 LLM,实现深度定制。
2.2 第一步:数据源获取与处理 —— 构建混合知识库
一个高质量的知识库需要兼具广度与深度。因此,我们将构建一个混合知识库,融合通用知识与垂直领域的专业知识。
通用知识:处理本地维基百科数据
维基百科是获取广博通用知识的绝佳来源。我们将下载完整的维基百科数据转储,并在本地进行处理。
下载数据转储:访问维基百科官方数据转储网站(例如
dumps.wikimedia.org),下载最新版本的中文维基百科页面文章转储文件,通常是.xml.bz2格式 20。提取纯文本:使用
wikiextractor这个强大的 Python 库来解析庞大的 XML 文件并提取干净的文本内容。这个工具可以去除维基标记、模板等无关信息。推荐使用以下命令 20:Bash
pip install wikiextractor wikiextractor -o wiki_text --json --no-templates zhwiki-latest-pages-articles.xml.bz2此命令会将处理后的纯文本以 JSON 格式输出到
wiki_text目录中。文本分块(Chunking):直接将整篇文章作为知识单元是不现实的,这会导致检索效率低下且上下文过于宽泛。必须将长文本分割成更小的、语义完整的块。LangChain 的
RecursiveCharacterTextSplitter是一个理想的工具,它会尝试按段落、句子等层级进行智能分割,以最大程度地保留上下文 22。将处理后的文本块保存,为下一步的向量化做准备 23。
专业知识:整合宠物领域数据集
为了让代理在宠物领域表现出专业性,必须喂给它垂直领域的专业数据。我们从公开数据平台 Hugging Face 和 Kaggle 上选取了几个高质量的数据集。
来源一:宠物健康症状数据集 (Hugging Face)
数据集:
karenwky/pet-health-symptoms-dataset25。价值:这个数据集极为宝贵,因为它包含了两种截然不同的文本风格:“主人观察描述”(Owner Observation)和“临床笔记”(Clinical Notes)。前者是通俗易懂的日常用语(如“我家猫不停地挠痒”),后者则是专业的兽医术语(如“瘙痒伴随脱毛”)。这为我们生成不同风格和口吻的小红书文案提供了绝佳的素材。
处理方式:将每一条记录的
text字段提取出来,作为独立的知识文档。
来源二:动物疾病与症状数据集 (Kaggle)
数据集:
shijo96john/animal-disease-prediction26 和willianoliveiragibin/animal-condition27。价值:这些数据集以结构化的形式提供了动物类型、症状、疾病等信息。它们是生成“干货”、“科普”和“避坑指南”类小红书内容的完美原料。
处理方式:将这些结构化数据转换为自然的文本描述。例如,可以将一条记录
{Animal_Type: 'Dog', Symptom_1: 'Vomiting', Symptom_2: 'Lethargy', Disease_Prediction: 'Gastritis'}转换为一段文本:“当一只狗出现呕吐和精神萎靡的症状时,可能需要警惕胃炎的风险。及时观察并咨询兽医非常重要。” 这样的文本片段可以直接被 RAG 系统检索和使用。
通过结合维基百科的通用知识和这些高度专业的宠物数据集,我们为 AI 代理构建了一个既有广度又有深度的强大知识后盾。
2.3 第二步:中文文本的本地向量化
在数据准备就绪后,我们需要将这些文本知识转换成机器能够理解和比较的数学形式——向量(Vectors)。这个过程称为嵌入(Embedding)。由于我们的应用场景是中文且要求完全离线,因此选择一个性能优异、可本地部署的中文 Embedding 模型至关重要。
模型选择
模型选择的核心依据是其在权威中文评测基准 C-MTEB (Chinese Massive Text Embedding Benchmark) 上的表现,同时需要兼顾模型的尺寸和资源消耗 28。目前,Hugging Face 上有多个优秀的开源选项。
BAAI/bge 系列:由北京智源人工智能研究院(BAAI)开发的
bge(Beijing AI General-purpose Embedding) 模型,特别是bge-large-zh-v1.5,在 C-MTEB 排行榜上长期处于领先地位,是中文文本表征的黄金标准之一 30。Alibaba-NLP/gte 系列:由阿里巴巴达摩院开发的
gte(General Text Embedding) 模型,特别是基于 Qwen2 大模型微调的gte-Qwen2-1.5B-instruct或gte-Qwen2-7B-instruct,展现了极强的多语言能力和上下文理解能力,在 C-MTEB 上同样名列前茅 28。
为了在性能和资源占用之间取得平衡,对于大多数本地部署场景,BAAI/bge-large-zh-v1.5 或 Alibaba-NLP/gte-Qwen2-1.5B-instruct 是绝佳的选择。它们提供了顶级的性能,同时对硬件的要求相对可控。
| 模型名称 (Model Name) | 基础模型 (Base Model) | C-MTEB 平均分 | 嵌入维度 (Dimension) | 最大序列长度 | 模型大小 (Size) | 检索查询指令 (Query Instruction) |
|---|---|---|---|---|---|---|
| BAAI/bge-large-zh-v1.5 | - | 64.53 | 1024 | 512 | ~1.3GB | 为这个句子生成表示以用于检索相关文章: |
| Alibaba-NLP/gte-Qwen2-1.5B-instruct | Qwen2-1.5B | 67.65 | 1536 | 32k | ~6.6GB | 无 (None) |
| jinaai/jina-embeddings-v2-base-zh | JinaBERT | - | 768 | 8192 | ~0.6GB | 无 (None) |
| m3e-base | BERT | 61.33 | 768 | 512 | ~0.4GB | 无 (None) |
数据综合自 28 及 C-MTEB 排行榜。分数可能会有小幅变动。
本地实现
我们将使用 sentence-transformers 库来加载和使用这些模型。为了确保完全离线,最佳实践是先将模型文件手动下载到本地目录。
Python
from sentence_transformers import SentenceTransformer
import torch
# 确定设备 (优先使用 GPU)
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using device: {device}")
# 指定本地模型路径 (假设已提前从 Hugging Face Hub 下载)
# 例如,使用 git lfs clone https://huggingface.co/BAAI/bge-large-zh-v1.5./models/bge-large-zh-v1.5
local_model_path = "./models/bge-large-zh-v1.5"
# 加载本地模型
# 对于 bge 模型,可以设置 normalize_embeddings=True 以便后续使用余弦相似度
embedding_model = SentenceTransformer(local_model_path, device=device)
print("Embedding model loaded successfully.")
# 示例文本
texts_to_embed = [
"如何给猫咪剪指甲?",
"幼犬需要打哪些疫苗?"
]
# 生成向量
embeddings = embedding_model.encode(texts_to_embed, normalize_embeddings=True)
print("Embeddings shape:", embeddings.shape)
# 输出应为 (2, 1024),代表2个文本,每个文本被编码为1024维的向量
这段代码展示了如何从本地文件夹加载一个预先下载好的模型,并用它来将文本编码为向量 32。
2.4 第三步:本地向量存储与索引
生成了文本向量之后,我们需要一个高效的数据库来存储它们,并支持快速的相似度检索。这个数据库被称为向量数据库(Vector Database)。对于本地化部署,我们有两个出色的开源选择:ChromaDB 和 FAISS。
推荐方案:ChromaDB
对于刚开始接触向量数据库的开发者,ChromaDB 是一个极佳的选择。它以其简洁的 API 和易于上手的特性而著称,并且能够轻松实现本地持久化存储 34。
以下是使用 Python 设置一个本地持久化 ChromaDB 的完整步骤 34:
安装 ChromaDB:
Bash
pip install chromadb注意:ChromaDB 可能对 SQLite 版本有要求,如果遇到问题,请确保 Python 环境和库版本兼容 34。
初始化持久化客户端:
为了让数据在程序关闭后依然存在,我们必须使用 PersistentClient,并指定一个本地目录来存储数据库文件 35。
Python
import chromadb # 指定一个目录用于持久化存储数据 db_path = "./chroma_db" # 初始化持久化客户端 client = chromadb.PersistentClient(path=db_path) print("ChromaDB client initialized for persistent storage.")创建或获取集合(Collection):
集合在 ChromaDB 中类似于传统数据库中的“表”,用于组织和管理一组特定的向量。使用 get_or_create_collection 方法可以避免重复创建 35。
Python
# 集合名称 collection_name = "pet_knowledge_base" # 获取或创建集合 # 我们可以传入 embedding_function,但由于我们已自行生成 embedding,后续会直接存入 collection = client.get_or_create_collection(name=collection_name) print(f"Collection '{collection_name}' ready. Total items: {collection.count()}")数据入库(Ingestion):
现在,我们将之前处理好的文本块及其对应的向量批量添加到集合中。为每个文档块提供一个唯一的 ID 是非常重要的,这有助于后续的更新或删除操作 34。
Python
# 假设 all_chunks 是一个包含所有文本块的列表 # 假设 all_embeddings 是一个包含对应向量的 numpy 数组 # 假设 all_metadata 是一个包含元数据的列表,例如 {"source": "wikipedia_cat_care"} # 生成唯一的 ID all_ids = [f"chunk_{i}" for i in range(len(all_chunks))] # 批量添加数据到集合中 # 为获得最佳性能,建议分批次 (e.g., 每批 100-500条) 添加 batch_size = 100 for i in range(0, len(all_chunks), batch_size): collection.add( embeddings=all_embeddings[i:i+batch_size].tolist(), # ChromaDB 接受 list 格式 documents=all_chunks[i:i+batch_size], metadatas=[{"source": "pet_knowledge"}] * len(all_chunks[i:i+batch_size]), # 示例元数据 ids=all_ids[i:i+batch_size] ) print(f"Added batch {i//batch_size + 1}, total items in collection: {collection.count()}") print("All data has been successfully ingested into ChromaDB.")
备选方案:FAISS
FAISS (Facebook AI Similarity Search) 是一个由 Meta 开发的高性能向量相似度搜索库 39。它提供了极致的检索速度,尤其是在处理海量数据集时。但其 API 相对底层和复杂,更适合对性能有极致要求的资深开发者 40。
IndexFlatL2:提供精确的、暴力的 L2 距离搜索。结果最准确,但速度最慢,适合中小型数据集 40。IndexIVFPQ:一种基于“倒排文件系统+乘积量化”的索引。它通过聚类和量化对向量进行压缩和分区,实现近似最近邻搜索(ANN),以微小的精度损失换取数量级的速度提升和内存节省,是处理大规模数据集的首选 40。
对于本项目,ChromaDB 的易用性和足够强大的性能已经完全可以满足需求。但了解 FAISS 为未来可能的性能扩展提供了方向 42。
至此,我们已经成功构建了一个完全离线、包含丰富宠物知识的 RAG 核心。下一步,我们将设计并实现使用这个知识库的 LangGraph 代理。
第三部分:架构设计:小红书“宠物博主”代理
拥有了强大的知识核心后,我们需要设计代理的“大脑”和“工作流程”。本章将深入分析小红书平台的内容特性,并将一个成功的“宠物博主”的创作过程,解构成一个由 LangGraph 实现的、结构化的、智能化的图状工作流。
3.1 解构小红书爆款宠物笔记
要让 AI 生成的内容在小红书上受欢迎,首先必须理解平台的“游戏规则”和用户偏好。通过分析大量成功案例,我们可以总结出爆款宠物笔记的几个关键特征:
真实性与故事化叙事:小红书是一个强调“分享”而非“营销”的社区。用户极其青睐那些以第一人称视角讲述的、充满真情实感的个人故事或经验分享 43。内容必须听起来像一个真实的朋友在和你聊天,而不是冷冰冰的广告 45。例如,成功的宠物食品品牌“诚实一口”正是通过“拟人化的内容创作”,将宠物作为故事主角,引发用户强烈的情感共鸣,从而脱颖而出 47。
视觉冲击力:小红书是高度视觉驱动的平台。笔记的封面图是决定用户是否点击的关键第一步 43。虽然我们的代理主要生成文本,但它在设计上应能提出视觉概念建议,以配合文案。
提供实用价值:纯粹的情感分享之外,能提供具体帮助的“干货”内容极受欢迎。例如,分享实用的养宠技巧、产品评测、避坑指南等,都能显著提升用户的收藏和点赞意愿 44。
吸睛的标题:标题是内容的“第二张脸”。成功的标题往往运用数字(如“5个必备好物”)、表情符号(增加亲和力)、疑问句(引发好奇)和强烈的情绪词(如“惊呆了”、“巨好用”)来吸引眼球 46。
精准的 SEO 策略:小红书的用户行为高度依赖搜索。因此,在标题和正文中自然地融入高频搜索关键词,并使用精准的、小众的标签(Hashtags),对于内容能否被目标用户发现至关重要 43。
基于以上分析,我们的 AI 代理不应是一个简单的“文章生成器”,而应是一个懂得小红书内容生态的“策略师”和“创作者”的结合体。
3.2 设计代理图谱:一个多节点协作的创作流
我们将采用“分而治之”的设计思想,将复杂的创作任务分解为一系列由专业节点负责的子任务。这种架构模仿了一个人类创意团队的协作模式(如研究员、撰稿人、编辑、SEO 专家),是 LangGraph 的核心优势之一 12。
代理状态 (PetPostState)
首先,我们定义代理的中央“记忆体”,即状态对象。它将贯穿整个创作流程,记录每一个环节的产出。
Python
from typing import TypedDict, List, Annotated
import operator
class PetPostState(TypedDict):
"""
定义了宠物内容创作代理的状态图。
这个状态对象会在图的节点之间传递和更新。
"""
# 输入
topic: str # 用户指定的主题,例如 "如何安抚一只应激的猫咪"
# 中间产物
research_material: List[str] # 从 RAG 知识库检索到的相关资料
initial_draft: str # 由撰稿节点生成的初稿
edited_draft: str # 经过小红书风格优化的编辑稿
# 评估与反馈 (实现循环的关键)
revision_notes: Annotated[list, operator.add] # 存储“创意总监”的修改意见
# 输出组件
title_options: List[str] # 多个备选的爆款标题
hashtag_suggestions: List[str] # 推荐的小红书标签
# 最终成品
final_post: str # 整合了标题、正文和标签的最终帖子内容
节点定义(“专家团队”)
接下来,我们定义图中的各个“专家”节点:
researcher(研究员):输入:
topic任务: 接收用户指定的主题,调用我们之前构建的 RAG 管道(ChromaDB),检索出最相关的知识片段。
输出: 更新状态中的
research_material。
drafting_specialist(撰稿专家):输入:
research_material,topic,revision_notes(在第二轮及以后)任务: 基于检索到的资料和主题,撰写一篇内容详实、逻辑清晰的初稿。在后续的修改轮次中,它会参考
revision_notes中的修改意见来重写稿件。输出: 更新状态中的
initial_draft。
xhs_stylist(小红书风格师):输入:
initial_draft任务: 将逻辑清晰的初稿改写成符合小红书平台风格的文案。这包括:打散长句、增加表情符号、使用网络热词、营造亲切的对话感。
输出: 更新状态中的
edited_draft。
title_artist(标题艺术家):输入:
edited_draft任务: 根据最终确定的正文内容,运用爆款标题公式(数字、夸张、提问等),生成多个供用户选择的、吸引人的标题。
输出: 更新状态中的
title_options。
seo_expert(SEO 专家):输入:
edited_draft,topic任务: 分析正文内容和主题,提取核心关键词,并结合小红书的热门趋势,生成一系列推荐的 hashtags。
输出: 更新状态中的
hashtag_suggestions。
finalizer(定稿人):输入:
edited_draft,title_options,hashtag_suggestions任务: 将编辑后的正文、选定的标题(或第一个标题)以及推荐的标签组合成一篇完整、可直接发布的小红书笔记。
输出: 更新状态中的
final_post。
3.3 “创意总监”循环:实现智能质量控制
这是整个代理设计的核心,也是 LangGraph 强大能力的集中体现。我们将引入一个特殊的路由节点和一个条件边,来创建一个能够自我评估和修正的反馈循环。
路由节点 (creative_director)
这个节点扮演着“创意总监”的角色,其职责是审查稿件质量并决定下一步的走向。
实现: 它是一个调用 LLM 的函数。
Prompt 设计: 我们会给 LLM 一个非常明确的指令,并附上一个基于我们对小红书平台分析得出的质量检查清单。
“你是一位资深的小红书宠物内容运营总监。请严格按照以下标准审查这份草稿:
故事性与情感:文案是否以第一人称视角讲述了一个引人入胜的真实故事?情感是否真挚?
价值与实用性:是否为读者提供了清晰、有用的养宠知识或技巧?
平台风格:语言是否口语化、亲切?是否恰当使用了表情符号来增强表达力?
整体质量:内容是否可以直接发布?
草稿内容如下:
{edited_draft}
请根据审查结果,仅返回以下两个词中的一个:‘APPROVE’ 或 ‘REVISE’。
如果你的决定是 ‘REVISE’,请在下一行附上 1-2 条最关键、最具体的修改意见,用于指导作者重写。”
状态更新: LLM 返回的修改意见将被追加到状态的
revision_notes列表中。
关键的条件边
现在,我们使用 add_conditional_edge 来构建这个智能循环 1:
起始节点:
creative_director决策逻辑: 条件边会检查
creative_director节点的输出。如果输出为 ‘APPROVE’:工作流被认为是高质量的,可以继续前进。图将流转到
title_artist和seo_expert节点,进行后续的标题和标签生成。如果输出为 ‘REVISE’:稿件质量不达标,需要修改。图将流转回
drafting_specialist节点。此时,drafting_specialist在新一轮的执行中,可以从状态中读取到creative_director提供的具体revision_notes,从而进行有针对性的、更高质量的重写。
这种设计体现了 LangGraph 的真正威力:它能够创建出具备自我反思和迭代改进能力的系统,代理会不断地“打磨”自己的作品,直到达到预设的质量标准为止。这种架构将一个简单的生成任务,提升为了一个动态的、智能的创作过程。
这种模仿人类创意团队协作的、基于角色的多代理系统架构,是解决复杂创意任务的最高效模式。单一的、试图一次性完成所有任务的“万能提示词”,往往只能产出平庸、泛化的结果。软件工程中的“关注点分离”原则在此同样适用。通过将复杂的任务分解为一系列更小、更专业的子任务,我们可以为每个环节设计高度优化的提示词和逻辑。LangGraph 的节点结构天然地契合了这种分解思想,每个节点都成为一个“专家代理”,而 StateGraph 则充当了所有专家协作时共享的“项目看板”或“草稿文件”。由“创意总监”节点控制的条件边,则扮演了项目经理的角色,在专家之间分配任务,并最终决定项目是否完成。这种架构不仅更强大,也更易于调试和扩展,标志着从“提示词工程”到更高级的“工作流工程”的转变。
第四部分:完整实现与代码走查
理论和设计已经就绪,现在是时候将它们转化为真实可运行的代码了。本章节将提供一个完整的、详细注释的 Python 项目,一步步带您实现我们所设计的“小红书宠物博主”AI 代理。
4.1 环境配置
首先,我们需要配置好项目的运行环境。
创建虚拟环境:为了保持项目依赖的隔离性,强烈建议创建一个新的 Python 虚拟环境。
python -m venv .venv source .venv/bin/activate # On Windows, use `.venv\Scripts\activate`安装依赖:创建一个
requirements.txt文件,并填入以下所有必需的库。
# requirements.txt
# 核心框架
langgraph
langchain
langchain-community
langchain-openai # 或者 langchain-anthropic, langchain-google-genai 等
# 本地 LLM 与 Embedding
ollama
sentence-transformers
# 向量数据库
chromadb
faiss-cpu # 虽然主要用Chroma, 但faiss是langchain中一个常见的依赖
# 数据处理
pandas
wikiextractor
# 其他工具
python-dotenv
ipython # 用于可视化
pyarrow # duckdb+parquet 依赖
然后通过 pip 一键安装:
pip install -r requirements.txt
设置本地 LLM (Ollama):确保您已经安装并运行了 Ollama。我们需要拉取一个强大的语言模型作为代理的“大脑”。推荐使用 Qwen 或 Llama 3 系列的模型。
ollama pull qwen2:7b-instruct
4.2 脚本一:build_knowledge_base.py
这个脚本是一次性运行的,其唯一目的是构建并持久化我们的 RAG 知识库。将数据处理与代理逻辑分离是一个良好的工程实践。
# build_knowledge_base.py
import os
import pandas as pd
from langchain_community.document_loaders import DirectoryLoader, TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from sentence_transformers import SentenceTransformer
import chromadb
import torch
import logging
# --- 配置 ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# 数据源路径
# 假设你已经处理好维基百科和Kaggle数据,并将它们存为txt文件
KNOWLEDGE_BASE_DIR = "./pet_knowledge_data"
# ChromaDB 持久化路径
CHROMA_DB_PATH = "./chroma_db"
# 集合名称
COLLECTION_NAME = "pet_knowledge_base"
# 本地 Embedding 模型
EMBEDDING_MODEL_NAME = "BAAI/bge-large-zh-v1.5"
# 批处理大小
BATCH_SIZE = 100
# --- 1. 加载和分割文档 ---
def load_and_split_documents(data_dir):
"""加载目录中的所有.txt文件并进行分割"""
logging.info(f"Loading documents from {data_dir}...")
# 使用TextLoader确保编码正确
loader = DirectoryLoader(data_dir, glob="**/*.txt", loader_cls=TextLoader, loader_kwargs={'encoding': 'utf-8'})
documents = loader.load()
if not documents:
logging.error("No documents found. Please check the data directory.")
return
logging.info(f"Loaded {len(documents)} documents. Splitting into chunks...")
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", "。", "!", "?", ",", "、", ""]
)
chunks = text_splitter.split_documents(documents)
logging.info(f"Split into {len(chunks)} chunks.")
return chunks
# --- 2. 初始化 Embedding 模型 ---
def initialize_embedding_model(model_name):
"""初始化并加载本地 Sentence Transformer 模型"""
logging.info(f"Initializing embedding model: {model_name}")
device = "cuda" if torch.cuda.is_available() else "cpu"
model = SentenceTransformer(model_name, device=device)
logging.info(f"Embedding model loaded on {device}.")
return model
# --- 3. 初始化 ChromaDB ---
def initialize_chromadb(db_path, collection_name):
"""初始化持久化的 ChromaDB 客户端和集合"""
logging.info(f"Initializing ChromaDB at {db_path}")
client = chromadb.PersistentClient(path=db_path)
collection = client.get_or_create_collection(name=collection_name)
logging.info(f"ChromaDB collection '{collection_name}' ready.")
return collection
# --- 4. 数据入库 ---
def ingest_data(collection, chunks, model):
"""将文档块向量化并存入 ChromaDB"""
if not chunks:
logging.warning("No chunks to ingest.")
return
logging.info(f"Starting data ingestion for {len(chunks)} chunks...")
# 清空旧集合以重新构建
if collection.count() > 0:
logging.warning("Existing collection is not empty. Clearing it before ingestion.")
collection.delete(ids=collection.get(include=)['ids'])
total_chunks = len(chunks)
for i in range(0, total_chunks, BATCH_SIZE):
batch_chunks = chunks
# 提取文本内容
batch_texts = [chunk.page_content for chunk in batch_chunks]
# 生成向量
embeddings = model.encode(batch_texts, normalize_embeddings=True).tolist()
# 提取元数据
metadatas = [chunk.metadata for chunk in batch_chunks]
# 生成唯一 ID
ids = [f"chunk_{i+j}" for j in range(len(batch_chunks))]
# 添加到集合
collection.add(
embeddings=embeddings,
documents=batch_texts,
metadatas=metadatas,
ids=ids
)
logging.info(f"Ingested batch {i//BATCH_SIZE + 1}/{(total_chunks + BATCH_SIZE - 1)//BATCH_SIZE}. "
f"Total items in collection: {collection.count()}")
logging.info("Data ingestion complete.")
# --- 主函数 ---
if __name__ == "__main__":
# 确保数据目录存在
if not os.path.exists(KNOWLEDGE_BASE_DIR):
os.makedirs(KNOWLEDGE_BASE_DIR)
logging.error(f"Data directory '{KNOWLEDGE_BASE_DIR}' was not found. "
f"An empty one has been created. Please add your.txt knowledge files there.")
else:
documents_chunks = load_and_split_documents(KNOWLEDGE_BASE_DIR)
embedding_model = initialize_embedding_model(EMBEDDING_MODEL_NAME)
chroma_collection = initialize_chromadb(CHROMA_DB_PATH, COLLECTION_NAME)
ingest_data(chroma_collection, documents_chunks, embedding_model)
4.3 脚本二:xiaohongshu_agent.py
这是项目的核心,包含了 LangGraph 代理的完整定义和执行逻辑。
# xiaohongshu_agent.py
import os
from typing import TypedDict, List, Annotated
import operator
from langchain_community.chat_models import ChatOllama
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_community.vectorstores import Chroma
from sentence_transformers import SentenceTransformer
import chromadb
import torch
from langgraph.graph import StateGraph, END
# --- 1. 环境与模型设置 ---
# 使用 dotenv 管理密钥(如果需要)
# from dotenv import load_dotenv
# load_dotenv()
# 初始化本地 LLM
local_llm = ChatOllama(model="qwen2:7b-instruct", temperature=0.7)
# 初始化 RAG 的 retriever
CHROMA_DB_PATH = "./chroma_db"
COLLECTION_NAME = "pet_knowledge_base"
EMBEDDING_MODEL_NAME = "BAAI/bge-large-zh-v1.5"
device = "cuda" if torch.cuda.is_available() else "cpu"
embedding_model = SentenceTransformer(EMBEDDING_MODEL_NAME, device=device)
client = chromadb.PersistentClient(path=CHROMA_DB_PATH)
vectorstore = Chroma(
client=client,
collection_name=COLLECTION_NAME,
embedding_function=None # 我们将手动生成查询向量
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
def query_embedding_function(text: str):
"""为查询文本生成向量的辅助函数"""
return embedding_model.encode(text, normalize_embeddings=True).tolist()
# --- 2. 定义代理状态 ---
class PetPostState(TypedDict):
topic: str
research_material: List[str]
initial_draft: str
edited_draft: str
revision_notes: Annotated[list, operator.add]
title_options: List[str]
hashtag_suggestions: List[str]
final_post: str
# --- 3. 定义图的节点(专家团队) ---
# 节点 1: 研究员
def researcher(state: PetPostState):
print("---NODE: Researcher---")
topic = state["topic"]
# 使用自定义的 embedding function 进行检索
query_vector = query_embedding_function(f"关于“{topic}”的小红书实用干货文章")
retrieved_docs = retriever.invoke(topic) # LangChain retriever 会自动处理 embedding
research_summary = "\n\n".join([doc.page_content for doc in retrieved_docs])
return {"research_material": [research_summary]}
# 节点 2: 撰稿专家
def drafting_specialist(state: PetPostState):
print("---NODE: Drafting Specialist---")
topic = state["topic"]
research_material = "\n".join(state["research_material"])
revision_notes = state.get("revision_notes",)
prompt_text = f"""你是一位专业的宠物博主内容撰稿人。请根据以下主题和研究资料,撰写一篇内容详实、逻辑清晰的初稿。
主题:{topic}
研究资料:
{research_material}
"""
if revision_notes:
notes_str = "\n".join(revision_notes)
prompt_text += f"\n请特别注意,上一轮的修改意见如下,请在本次重写中解决这些问题:\n{notes_str}"
prompt = ChatPromptTemplate.from_template(prompt_text)
chain = prompt | local_llm | StrOutputParser()
initial_draft = chain.invoke({})
return {"initial_draft": initial_draft}
# 节点 3: 小红书风格师
def xhs_stylist(state: PetPostState):
print("---NODE: Xiaohongshu Stylist---")
initial_draft = state["initial_draft"]
prompt = ChatPromptTemplate.from_template(
"""你是一位小红书爆款内容优化师。请将以下初稿改写成一篇充满小红书风格的笔记。要求:
1. 使用大量生动有趣的表情符号。
2. 将长句拆分为短句,段落之间用空行隔开,增加呼吸感。
3. 语气要亲切、口语化,像在和朋友聊天。
4. 在文案开头和结尾增加引导互动的话语。
初稿:
{draft}"""
)
chain = prompt | local_llm | StrOutputParser()
edited_draft = chain.invoke({"draft": initial_draft})
return {"edited_draft": edited_draft}
# 节点 4: 创意总监 (路由节点)
def creative_director(state: PetPostState):
print("---NODE: Creative Director (Router)---")
edited_draft = state["edited_draft"]
prompt = ChatPromptTemplate.from_template(
"""你是一位资深的小红书宠物内容运营总监。请严格按照以下标准审查这份草稿,并决定是“批准”还是“修改”。
1. 故事性与情感:文案是否以第一人称视角讲述了一个引人入胜的真实故事?情感是否真挚?
2. 价值与实用性:是否为读者提供了清晰、有用的养宠知识或技巧?
3. 平台风格:语言是否口语化、亲切?是否恰当使用了表情符号来增强表达力?
4. 整体质量:内容是否可以直接发布?
草稿内容如下:
```{draft}```
请根据审查结果,仅返回以下两个词中的一个:'APPROVE' 或 'REVISE'。
如果你的决定是 'REVISE',请在下一行附上 1-2 条最关键、最具体的修改意见,用于指导作者重写。"""
)
chain = prompt | local_llm | StrOutputParser()
response = chain.invoke({"draft": edited_draft})
if "APPROVE" in response:
print("---DECISION: Draft Approved---")
return "approve"
else:
print("---DECISION: Draft Needs Revision---")
# 提取修改意见
revision_note = response.replace("REVISE", "").strip()
state["revision_notes"].append(revision_note) # 直接修改状态以传递意见
return "revise"
# 节点 5: 标题艺术家
def title_artist(state: PetPostState):
print("---NODE: Title Artist---")
edited_draft = state["edited_draft"]
prompt = ChatPromptTemplate.from_template(
"你是一位小红书爆款标题专家。请根据以下正文,生成5个吸引眼球、可能成为爆款的小红书标题。每个标题独立一行,不要加序号。\n\n正文:\n{draft}"
)
chain = prompt | local_llm | StrOutputParser()
titles = chain.invoke({"draft": edited_draft}).split("\n")
return {"title_options": [t for t in titles if t]}
# 节点 6: SEO 专家
def seo_expert(state: PetPostState):
print("---NODE: SEO Expert---")
edited_draft = state["edited_draft"]
prompt = ChatPromptTemplate.from_template(
"你是一位小红书SEO专家。请根据以下正文,提取核心关键词,并生成10个最相关的小红书hashtags。用空格分隔。\n\n正文:\n{draft}"
)
chain = prompt | local_llm | StrOutputParser()
hashtags = chain.invoke({"draft": edited_draft}).strip().split()
return {"hashtag_suggestions": hashtags}
# 节点 7: 定稿人
def finalizer(state: PetPostState):
print("---NODE: Finalizer---")
title = state["title_options"] if state["title_options"] else "【请填写标题】"
content = state["edited_draft"]
hashtags = " ".join([f"#{tag}" for tag in state["hashtag_suggestions"]])
final_post = f"{title}\n\n{content}\n\n{hashtags}"
return {"final_post": final_post}
# --- 4. 构建图 ---
workflow = StateGraph(PetPostState)
# 添加节点
workflow.add_node("researcher", researcher)
workflow.add_node("drafting_specialist", drafting_specialist)
workflow.add_node("xhs_stylist", xhs_stylist)
workflow.add_node("creative_director", creative_director)
workflow.add_node("title_artist", title_artist)
workflow.add_node("seo_expert", seo_expert)
workflow.add_node("finalizer", finalizer)
# 设置入口
workflow.set_entry_point("researcher")
# 添加边
workflow.add_edge("researcher", "drafting_specialist")
workflow.add_edge("drafting_specialist", "xhs_stylist")
workflow.add_edge("xhs_stylist", "creative_director")
# 添加条件边 (核心循环)
workflow.add_conditional_edges(
"creative_director",
creative_director, # 使用节点函数本身作为条件判断
{
"approve": "title_artist",
"revise": "drafting_specialist" # 如果需要修改,则返回撰稿节点
}
)
# 并行节点后的汇合
workflow.add_edge("title_artist", "seo_expert") # 顺序执行,也可以并行后汇合
workflow.add_edge("seo_expert", "finalizer")
workflow.add_edge("finalizer", END)
# 编译图
app = workflow.compile()
# --- 5. 可视化与执行 ---
# 可视化图
try:
from IPython.display import Image, display
png_data = app.get_graph().draw_mermaid_png()
with open("xiaohongshu_agent_graph.png", "wb") as f:
f.write(png_data)
print("Graph visualization saved to xiaohongshu_agent_graph.png")
except ImportError:
print("Please install 'ipython' and 'pygraphviz' to visualize the graph.")
# 执行代理
if __name__ == "__main__":
topic_input = "如何帮助新来的小猫适应环境,减少对家里原住民大猫的压力?"
inputs = {"topic": topic_input, "revision_notes":}
print("\n--- STARTING AGENT EXECUTION ---")
for event in app.stream(inputs, {"recursion_limit": 5}):
for key, value in event.items():
print(f"--- Event: {key} ---")
# print(value) # 打印完整状态,用于调试
# 获取最终结果
final_state = app.invoke(inputs, {"recursion_limit": 5})
print("\n\n--- FINAL XIAOHONGSHU POST ---")
print(final_state["final_post"])
4.4 可视化图谱
运行 xiaohongshu_agent.py 脚本后,如果环境配置正确,将生成一个名为 xiaohongshu_agent_graph.png 的图片文件。这张图直观地展示了我们设计的代理工作流,包括其核心的循环结构,对于理解和调试代理的行为非常有价值 17。
这张图清晰地揭示了从研究到草稿,再到风格化,然后进入“创意总监”的决策点。如果稿件被批准,流程将继续到标题和SEO优化,最后定稿。如果被驳回,流程将带着修改意见返回到撰稿专家节点,形成一个闭环,直到产出高质量的内容为止。
第五部分:总结、高级技巧与未来展望
通过前面的章节,我们不仅深入探讨了 LangGraph 的核心思想,还亲手构建了一个功能完备、可离线运行的小红书内容创作代理。本章将对整个项目进行总结,并介绍几种可以进一步增强代理能力的高级技巧,为您的 AI 代理工程之路指明未来的方向。
5.1 总结:有状态代理的力量
本项目成功地展示了 LangGraph 框架的强大之处。我们构建的代理不再是一个简单的、线性的“提示-响应”机器,而是一个能够执行复杂、多步骤创作任务的智能系统。其核心优势体现在:
状态化记忆:通过
StateGraph,代理在整个创作流程中维持了一个统一的“记忆”,使得不同功能的节点(专家)可以基于前序工作的结果进行协作。循环与迭代:通过
add_conditional_edge实现的“创意总监”反馈循环,是代理智能的源泉。它赋予了代理自我评估和迭代优化的能力,这对于要求高质量输出的创意任务至关重要。模块化与可控性:将复杂的任务分解为一系列独立的节点,使得整个工作流清晰、可控,并且易于调试和扩展。
可以明确地说,对于任何需要规划、反思和自我修正的复杂代理行为,LangGraph 提供了一个远比传统线性链条更为优雅和强大的解决方案。
5.2 高级技巧一:引入“人机协作” (Human-in-the-Loop)
尽管我们的“创意总监”节点可以自动把关质量,但在许多生产场景中,最终的决策权仍然需要掌握在人类手中。LangGraph 的设计原生支持“人机协作”模式 1。
实现思路
我们可以对图进行简单的修改,在最终定稿前插入一个“人工审核”节点。
添加
human_review节点:创建一个新的节点函数,它会打印出待发布的最终文案,并使用input()函数暂停执行,等待用户在控制台中输入 “yes” 或 “no”。Python
def human_review(state: PetPostState): print("---NODE: Human Review---") print("Please review the final post:") print("--------------------") print(state["final_post"]) print("--------------------") while True: feedback = input("Approve and finish? (yes/no): ").lower() if feedback in ["yes", "no"]: return {"human_feedback": feedback}修改图的边:
将原先从
finalizer指向END的边,改为指向human_review节点。从
human_review节点出发,添加一个新的条件边。如果用户输入 “yes”,则流向END;如果输入 “no”,则可以设计流回到drafting_specialist节点,并允许用户输入新的修改意见,加入到revision_notes中,从而实现由人类主导的修改循环。
5.3 高级技巧二:实现跨会话的持久化记忆
当前代理的“记忆”仅在单次运行中有效。要让代理变得更强大,比如记住它已经写过哪些主题以避免重复,我们需要让它的状态可以跨越多次运行而持久化。LangGraph 的**检查点(Checkpointer)**机制为此而生 6。
实现思路
使用 MemorySaver 可以轻松实现内存中的状态持久化。对于生产环境,还可以对接如 Redis、PostgreSQL 等外部存储。
导入并使用 Checkpointer:
Python
from langgraph.checkpoint.memory import MemorySaver memory_saver = MemorySaver() app = workflow.compile(checkpointer=memory_saver)使用线程 ID 管理会话:在调用代理时,通过
config参数传入一个可配置的thread_id。LangGraph 会使用这个 ID 来保存和加载特定会话的状态 50。Python
# 第一次为 "cat_food_post" 这个项目创建内容 config = {"configurable": {"thread_id": "cat_food_post"}} app.invoke({"topic": "如何挑选优质猫粮?"}, config=config) #...过了一段时间后... # 代理可以从上次中断的地方继续,或者查看历史状态 # 例如,如果上次任务中断了,再次调用 invoke 就会从中断的节点继续 # 也可以用 get_state 来查看历史 thread_state = app.get_state(config) print("Last known state for 'cat_food_post':", thread_state)通过这种方式,代理就拥有了长期记忆,能够管理多个并行的创作项目,并能在中断后恢复工作。
5.4 未来方向
我们构建的代理仅仅是一个起点。基于 LangGraph 灵活的架构,未来可以探索更多激动人心的可能性:
多模态内容生成:当前代理只生成文本。未来可以集成多模态模型(如 CLIP、DALL-E 3),让代理在生成文案的同时,能够理解并生成匹配的图片概念,甚至直接生成小红书封面图,打造完整的图文笔记。
自动化 A/B 测试:设计一个更复杂的图,让代理针对同一个主题,生成两种不同风格(例如,一个偏干货,一个偏情感故事)的文案和标题,并输出为 A/B 测试建议,帮助运营者优化内容策略。
动态 RAG 与网络搜索:当前的 RAG 知识库是静态的。可以为代理增加一个“决策”节点,让它判断当前问题是应该查询本地知识库,还是需要获取最新的网络信息(例如,最新的宠物用品趋势)。为此,可以给代理增加一个调用网络搜索的工具,使其知识永不过时。
平台趋势感知:通过接入小红书数据分析工具的 API,让代理能够感知到平台最新的热门话题和关键词,并主动建议创作方向,成为一个真正意义上的“智能运营伙伴”。
总之,LangGraph 为构建复杂、可靠、可控的 AI 代理提供了坚实的工程基础。通过不断地迭代和扩展我们所设计的图谱,其能力边界将远超我们今日所见。