From RAG to Agentic AI. How I Added LangGraph to My Local
From RAG to Agentic AI: How I Added LangGraph to My Local
在上一篇文章中,我构建了一个完全本地化的 RAG 助手,使用了 Ollama、ChromaDB 和 LangChain,并全部运行在 Docker 中。它通过搜索文档并引用来源来回答技术支持问题。它确实能工作,但在使用一段时间后,我注意到一个令人不适的问题:它对待所有问题的方式都是一样的。问它“如何关闭月度工资单?”它去搜索文档。没问题。问它“服务器启动时崩溃了”,它也去搜索文档。这就不太好了。问它一个完全不在文档范围内的问题,它还是去搜索文档。这毫无用处。真正的技术支持人员不会这样做。他们会先评估情况,然后决定采取什么行动:查阅资料、运行诊断,或者升级给人工处理。我的 RAG 系统缺乏这种判断力。本文的主题就是我如何利用 LangGraph 将该系统演进为 Agentic AI(代理式 AI)架构,让助手先决定使用哪种策略,然后再采取相应的行动。
The Core Limitation of Classic RAG
经典 RAG 的核心局限性
Classic RAG is a linear pipeline. Every query follows the exact same path: Question → Embed → Retrieve → Prompt → LLM → Answer. No branching. No decision-making. No memory between steps. This works perfectly for procedural questions where the answer lives in the docs. But technical support involves at least three distinct scenarios:
经典 RAG 是一个线性流水线。每个查询都遵循完全相同的路径:问题 → 向量化 → 检索 → 提示词 → 大模型 → 回答。没有分支,没有决策,步骤之间也没有记忆。这对于答案存在于文档中的程序性问题非常有效。但技术支持至少涉及三种不同的场景:
| Scenario | Example | Best strategy |
|---|---|---|
| Procedural question | ”How do I create an account?” | Search documentation |
| Known error code | ”ERR-COMP-001 appears” | Lookup error database |
| Unknown incident | ”Server crashes, no idea why” | Diagnose + escalate if needed |
| 场景 | 示例 | 最佳策略 |
|---|---|---|
| 程序性问题 | “我该如何创建账户?” | 搜索文档 |
| 已知错误代码 | “出现 ERR-COMP-001” | 查询错误数据库 |
| 未知故障 | “服务器崩溃,原因不明” | 诊断 + 必要时升级 |
A single RAG pipeline handles the first case well and the other two poorly. The solution is to add a layer of reasoning before retrieval.
单一的 RAG 流水线能很好地处理第一种情况,但后两种处理得很差。解决方案是在检索之前增加一层推理。
What Agentic AI Adds
Agentic AI 带来了什么
The shift from RAG to Agentic AI comes down to one thing: the system plans before it acts. Instead of one fixed pipeline, you have:
从 RAG 到 Agentic AI 的转变归结为一件事:系统在行动前会进行规划。你不再拥有一个固定的流水线,而是拥有:
Question ↓ Classifier (what kind of question is this?) ↓ ├── Procedural → RAG Agent (search docs) ├── Error code → Diagnostic Agent (lookup + LLM analysis) └── Complex → Diagnostic Agent → Escalation Agent (generate ticket)
问题 ↓ 分类器(这是什么类型的问题?) ↓ ├── 程序性 → RAG 代理(搜索文档) ├── 错误代码 → 诊断代理(查询 + 大模型分析) └── 复杂问题 → 诊断代理 → 升级代理(生成工单)
Each branch is an agent; an autonomous function that receives a shared state, does its job, updates the state, and returns it. LangGraph connects these agents into a directed graph with conditional routing between them.
每个分支都是一个代理;这是一个自主函数,它接收共享状态,完成工作,更新状态并将其返回。LangGraph 将这些代理连接成一个有向图,并在它们之间进行条件路由。
The Shared State Foundation of LangGraph
LangGraph 的共享状态基础
Before building agents, you define the state they all share. Think of it as a case file that every agent reads and updates:
在构建代理之前,你需要定义它们共享的状态。可以把它想象成每个代理都会读取和更新的“案卷”:
# src/agents/state.py
from typing import TypedDict, List, Annotated, Sequence
from langchain_core.messages import BaseMessage
import operator
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
question: str # Original question never changes
plan: List[str]
current_step: int
past_steps: List[str] # Audit trail of what each agent did
response: str
next_agent: str
niveau_support: int # 1=resolved, 2=partial, 3=needs human
The niveau_support field is key—it’s how agents communicate urgency to each other. If an agent sets it to 3, the orchestrator escalates automatically. Annotated[Sequence[BaseMessage], operator.add] means messages accumulate — each agent appends rather than overwrites. You get a full conversation trace at the end.
niveau_support 字段是关键,它是代理之间沟通紧急程度的方式。如果某个代理将其设置为 3,编排器会自动进行升级。Annotated[Sequence[BaseMessage], operator.add] 意味着消息会累积——每个代理都是追加而不是覆盖。最终你会得到完整的对话轨迹。
Agent 1: The RAG Agent
代理 1:RAG 代理
The pipeline from my previous article, repackaged as an autonomous agent:
将我上一篇文章中的流水线重新封装为一个自主代理:
# src/agents/rag_agent.py
def rag_agent(state: AgentState) -> AgentState:
"""Searches the documentation and generates a grounded answer."""
docs = vectorstore.similarity_search(state["question"], k=3)
if not docs:
return {
**state,
"messages": list(state["messages"]) + [AIMessage(content="[RAG] No relevant document found.")],
"past_steps": state["past_steps"] + ["RAG: no results"],
"niveau_support": 3 # Nothing found — escalate
}
context = "\n\n".join([f"[Source: {d.metadata.get('module', '?')}]\n{d.page_content}" for d in docs])
answer = llm.invoke(f"Answer from context only.\nContext: {context}\nQuestion: {state['question']}")
return {
**state,
"messages": list(state["messages"]) + [AIMessage(content=f"[RAG] {answer}")],
"past_steps": state["past_steps"] + ["RAG: documentation search completed"],
"niveau_support": 1
}
The critical difference from the classic RAG: the agent returns an updated state, not just an answer. Every agent follows this same pattern — receive state, do work, return updated state.
与经典 RAG 的关键区别在于:代理返回的是更新后的状态,而不仅仅是一个答案。每个代理都遵循相同的模式——接收状态、执行工作、返回更新后的状态。
Agent 2: The Diagnostic Agent
代理 2:诊断代理
Handles error codes and technical incidents with two modes:
通过两种模式处理错误代码和技术故障:
# src/agents/diagnostic_agent.py
KNOWN_ERRORS = {
"ERR-COMP-001": {"description": "Debit/credit imbalance", "solution": "Check Total Debit = Total Credit..."},
"ERR-PAIE-042": {"description": "Payroll line missing rate", "solution": "Go to Settings > Payroll Lines..."},
}
def diagnostic_agent(state: AgentState) -> AgentState:
question = state["question"]
found_codes = [code for code in KNOWN_ERRORS if code.lower() in question.lower()]
if found_codes:
# Mode 1 : known error: instant answer, no LLM call needed
responses = [f"Code {code}: {KNOWN_ERRORS[code]['description']}\nSolution: {KNOWN_ERRORS[code]['solution']}" for code in found_codes]
return {
**state,
"messages": list(state["messages"]) + [AIMessage(content=f"[Diagnostic] {chr(10).join(responses)}")],
"past_steps": state["past_steps"] + ["Diagnostic: known error, resolved"],
"niveau_support": 1
}
# Mode 2 : unknown error: LLM analysis
answer = llm.invoke(f"Analyze this technical issue. If unsure, say so.\nIssue: {question}")
return {
**state,
"messages": list(state["messages"]) + [AIMessage(content=f"[Diagnostic] {answer}")],
"past_steps": state["past_steps"] + ["Diagnostic: LLM analysis, uncertain"],
"niveau_support": 2
}
The known error lookup runs in milliseconds without touching the LLM; a meaningful performance gain for common incidents.
已知错误查询在几毫秒内即可完成,无需调用大模型;这对于常见故障来说是一个显著的性能提升。
Agent 3: The Escalation Agent
代理 3:升级代理
Called only when niveau_support >= 3. It doesn’t try to answer; it generates a structured ticket with the full diagnostic history already attached:
仅在 niveau_support >= 3 时调用。它不会尝试回答,而是生成一个结构化的工单,并附带完整的诊断历史记录:
# src/agents/escalation_agent.py
# ... (Implementation details)