前言

后面将会给现有的智能体做p0,p1,...,pX阶段的升级,完善系统功能,并修复漏洞。

1. 为什么要做 Agent v2 的 P0 升级

前一版系统已经可以通过 LangGraph 完成工具路由、工具执行和回答生成,但整体流程还有几个问题:

  • 工具路由只告诉系统选择了什么工具,没有解释为什么选择;
  • 工具执行失败或结果不可用时,缺少明确 fallback 路径;
  • RAG 检索到上下文后,系统默认继续回答,没有判断上下文是否足够支撑答案;
  • 前端 Agent Trace 展示的信息还不够完整。

所以这次 P0 升级的目标不是把项目改成复杂 autonomous Agent,而是让现有 RAG Agent Workflow 更可解释、更稳定。

P0-1:给 LLM Router 的 decision 增加 reason 字段

原来的路由结果主要关注 tool,也就是模型判断当前问题应该调用 ragcalculatortimeweb_search 还是 llm

但从调试和用户角度看,只知道“选择了哪个工具”还不够,还需要知道“为什么选择这个工具”,不然系统是个黑盒,完全不知道系统内部是如何工作和思考出来这个结果,总是让人感觉心里没有底。

因此这一步给 Router 的 decision 增加了 reason 字段,让模型在输出工具选择结果时,同时给出选择理由。

这样做有两个好处:

1.方便调试路由错误。
如果模型选错工具,可以通过 reason 判断它是误解了问题,还是规则设计不清楚。
2.方便前端展示。
Django 页面中的 Agent Trace 可以展示 route 和 reason,让用户看到系统为什么调用某个工具。

前面曾经给网络搜索工具的使用增加过硬约束,那个时候在使用网络搜索工具之前没有给出使用这个工具的理由,现在需要对app/graph/nodes.py文件中,网络工具的使用部分增加网络工具选择的硬约束增加的使用原因所以需要改maybe_force_web_search函数的返回结果为:

if has_web_signal and looks_like_local_doc:
    return {
        "tool": "web_search",
        "input": query,
        "reason": "The question contains web/current information signals and should use external search instead of only local documents."
    }

return decision

同样的,关于其他几个自建工具的路由硬约束也要增加相应的选择原因,改normalize_decision函数为下面的版本:

def normalize_decision(decision: dict, query: str, valid_tool_names: set[str]) -> dict:
    if not isinstance(decision, dict):
        return {
            "tool": "llm",
            "input": query,
            "reason": "Router output is not a valid JSON object, so the system falls back to the general LLM tool."
        }

    tool = str(decision.get("tool", "")).strip().lower()

    raw_input = decision.get("input", "")
    tool_input = "" if raw_input is None else str(raw_input).strip()

    raw_reason = decision.get("reason", "")
    reason = "" if raw_reason is None else str(raw_reason).strip()

    if not reason:
        reason = f"The router selected {tool or 'llm'} based on the question type."

    if tool not in valid_tool_names:
        return {
            "tool": "llm",
            "input": query,
            "reason": f"Router selected an invalid tool '{tool}', so the system falls back to the general LLM tool."
        }

    # 对 rag / llm / time / web_search,统一保留原始 query
    # 避免 router 自己把 input 改写成答案或过度改写问题
    if tool in {"rag", "llm", "time", "web_search"}:
        return {
            "tool": tool,
            "input": query,
            "reason": reason
        }

    # calculator 允许保留模型抽出来的数学表达式
    if tool == "calculator":
        if not tool_input:
            return {
                "tool": "calculator",
                "input": query,
                "reason": reason
            }

        return {
            "tool": "calculator",
            "input": tool_input,
            "reason": reason
        }

    return {
        "tool": "llm",
        "input": query,
        "reason": "The router result could not be normalized, so the system falls back to the general LLM tool."
    }

做完前面硬路由(我习惯叫硬路由,因为我觉得这种通过代码约束的方式是刚性的)的优化,后面还要继续做软路由(这里我习惯叫软路由,因为智能体的这种路由时基于LLM,返回的结果不是刚性的,如果用户问的问题比较模糊,那么路由结果也可能会不一样)的优化。这里的优化主要是集中在提示词的优化上,前面没有返回原因的时候提示词是这样的:

Rules:
1. You must return exactly one JSON object.
2. JSON format:
{{"tool": "...", "input": "..."}}
3. tool must be one of: rag, calculator, time, web_search, llm

如今要让路由返回工具选择的原因,所以要增加提示词,并且注意,以往路由有直接回答用户问题的BUG,所以在提示词上要做约束,做硬路由约束,也是为了避免模型没有进行路由选择,直接回答了用户的BUG:

Rules:
1. You must return exactly one JSON object.
2. JSON format:
{{"tool": "...", "input": "...", "reason": "..."}}
3. tool must be one of: rag, calculator, time, web_search, llm
4. reason must be one short sentence explaining why this tool is selected.
5. reason must not answer the user's question.
6. For rag, llm, time, and web_search:
   - input should stay the same as the user's original question
   - do not invent a new sentence
7. For calculator:
   - input should be the math expression only if you can extract it
8. Do not include markdown, explanations, or code fences.

随后,对项目的异常兜底返回值进行调整,以前的异常返回没有给出工具选择的原因,这里遭遇了异常,所以返回结果应相应的调整:

return {
    "decision": {
        "tool": "llm",
        "input": query,
        "reason": "Router failed, so the system falls back to the general LLM tool."
    },
    "error": f"choose_tool_node failed: {str(e)}"
}

P0-2:增加 LangGraph 条件边和 LLM Fallback

在原来的流程中,工具执行完成后通常会继续进入答案生成节点。但实际系统运行时,工具可能失败,RAG 也可能没有返回有效结果。

如果所有情况都强行进入 generate_answer,系统就容易出现两类问题:

  • 工具结果为空,但模型仍然尝试生成答案;
  • 工具调用失败后,流程缺少兜底路径。

因此这一步在 LangGraph 中加入了条件边,根据工具执行状态决定后续路径。

大致流程变成:

用户问题 → choose_tool → execute_tool → 判断工具结果 → generate_answer 或 llm_fallback

其中 llm_fallback 不是为了编造答案,而是在工具结果不可用时提供兜底回复。例如提醒用户当前工具调用失败,或者根据通用语言能力给出有限回答。

这一步的关键价值在于:

  • 工作流不再是固定直线流程;
  • 系统开始具备异常路径处理能力;
  • LangGraph 的条件边真正发挥作用;
  • Agent Trace 可以记录是否触发 fallback。

本次修改涉及到的需要修改和新增的有系统状态图app/graph/state.py,工作节点app/graph/nodes.pyapp/graph/builder.py以及app/main.py

在状态图中主要工作是新增三个关键字段,用于记录动态的状态流流转轨迹,以及在fallback节点中重试次数:

# 是否使用过 LLM fallback
fallback_used: bool

# fallback / retry 次数
retry_count: int

# 实际执行过的工作流节点路径
# 例如: ["choose_tool", "execute_tool", "llm_fallback", "generate_answer"]
workflow_path: list[str]

随后修改app/graph/nodes.py,这一步的工作属于:

  • LangGraph 编排层
  • AgentState 状态层
  • LLM fallback 节点层

主要工作有三点:

  1. choose_tool / execute_tool / generate_answer 记录 workflow_path
  2. 新增 route_after_execute
  3. 新增 llm_fallback_node

关于第一点工作主要是在每一个工作节点的开始增加一个字段,以执行工具节点为例,在该节点的开头增加如下字段,这样在节点被执行时,状态图的workflow_path就会记录工作流走过execute_tool节点:

workflow_path = state.get("workflow_path", []) + ["execute_tool"]

另外一个工作的小点在于每个节点的异常处理和返回值要加上对状态图的更新操作:

#返回,以工具选择节点为例
return {
    "decision": decision,
    "workflow_path": workflow_path
}

#异常处理,以工具选择节点为例
return {
    "decision": {
        "tool": "llm",
        "input": query,
        "reason": "Router failed, so the system falls back to the general LLM tool."
    },
    "error": f"choose_tool_node failed: {str(e)}",
    "workflow_path": workflow_path
}

下面是核心节点的创建,创建条件边需要创建可以提供选择的条件节点,这种节点可能叫状态驱动的条件路由函数更合适,所以在app/graph/nodes.py中创建可供条件选择的节点,该节点可以根据状态图中的error信息决定下一个工作的节点:

def route_after_execute(state: AgentState) -> str:
    if state.get("error") and not state.get("fallback_used"):
        logger.warning("[route_after_execute] error found, route to llm_fallback")
        return "llm_fallback"

    logger.info("[route_after_execute] no error, route to generate_answer")
    return "generate_answer"

同时还要补充一个llm_fallback_node,他的作用在于对于前发生的错误,用语言模型尝试重新回答:

def llm_fallback_node(state: AgentState) -> AgentState:
    workflow_path = state.get("workflow_path", []) + ["llm_fallback"]

    query = state.get("query", "")
    chat_history = state.get("chat_history", [])
    previous_error = state.get("error", "")
    retry_count = state.get("retry_count", 0) + 1

    logger.warning(f"[llm_fallback_node] fallback triggered. previous_error: {previous_error}")

    try:
        messages = [
            {
                "role": "system",
                "content": (
                    "You are a helpful assistant. "
                    "The previous tool execution failed, so you should answer the user directly. "
                    "If the question requires document evidence or external data that is unavailable, "
                    "clearly state the limitation instead of making unsupported claims."
                )
            }
        ]

        if chat_history:
            messages.extend(chat_history)

        messages.append({
            "role": "user",
            "content": query
        })

        response = client.chat.completions.create(
            model=CHAT_MODEL,
            messages=messages
        )

        answer = response.choices[0].message.content

        return {
            "tool_result": {
                "tool_name": "llm_fallback",
                "tool_input": query,
                "tool_output": answer
            },
            "fallback_used": True,
            "retry_count": retry_count,
            "error": "",
            "workflow_path": workflow_path
        }

    except Exception as e:
        logger.exception("llm_fallback_node failed")
        return {
            "tool_result": {
                "tool_name": "llm_fallback_error",
                "tool_input": query,
                "tool_output": "LLM fallback failed."
            },
            "fallback_used": True,
            "retry_count": retry_count,
            "error": f"llm_fallback_node failed: {str(e)}",
            "workflow_path": workflow_path
        }

最后在app/graph/builder.py中增加条件边,让工作流根据条件节点返回的结果选择不同的工作流分支:

from app.graph.nodes import (
    build_choose_tool_node,
    build_execute_tool_node,
    generate_answer_node,
    llm_fallback_node,
    route_after_execute,
)

graph_builder.add_edge(START, "choose_tool")
graph_builder.add_edge("choose_tool", "execute_tool")

graph_builder.add_conditional_edges(
    "execute_tool",
    route_after_execute,
    {
        "llm_fallback": "llm_fallback",
        "generate_answer": "generate_answer",
    }
)

graph_builder.add_edge("llm_fallback", "generate_answer")
graph_builder.add_edge("generate_answer", END)

最后的最后,给app/main.py添加一个刚刚新增在状态图中的几个字段即可:

agent_trace = {
    "route_decision": decision,
    "tool_used": tool_result.get("tool_name", ""),
    "tool_input": tool_result.get("tool_input", ""),
    "fallback_used": result.get("fallback_used", False),
    "error": result.get("error", ""),
    "retry_count": result.get("retry_count", 0),
    "workflow": result.get(
        "workflow_path",
        ["choose_tool", "execute_tool", "generate_answer"]
    )
}

P0-3:增加 context_sufficient 检查

RAG 系统中,一个常见问题是:检索到了内容,不代表内容一定足够回答问题。

如果系统只要检索到 chunk 就直接回答,可能会出现回答依据不足的问题。

所以 P0-3 增加了 context_sufficient 检查,用来判断当前 retrieved context 是否足够支撑后续回答。

第一版没有做复杂的 LLM-as-judge,而是采用轻量规则判断:

  • 如果当前工具不是 rag,可以默认不走上下文充分性检查;
  • 如果 rag 没有返回有效检索结果,则 context_sufficient=False;
  • 如果 retrieved context 数量或内容不足,则进入 fallback;
  • 如果检索结果满足基本要求,则继续 generate_answer。

这一步的价值是让 RAG 流程多了一层质量控制:

不是“检索到什么就答什么”,而是先判断上下文是否足够,再决定是否生成答案。

需要修改的关键函数有:

  • app/graph/state.py
  • app/rag_system.py
  • app/graph/nodes.py
  • app/main.py

对应层次:

app/graph/state.py
    -> AgentState 状态层

app/rag_system.py
    -> RAG 层,生成 context_sufficient

app/graph/nodes.py
    -> LangGraph 节点层,把 context_sufficient 写回最终 state

app/main.py
    -> FastAPI 接口层,把 context_sufficient 放进 agent_trace

首先,在状态图中增加判断RAG回答是否充分的字段:

# RAG 检索到的上下文是否足够支撑回答
context_sufficient: bool

然后修改app/rag_system.py中的ask_with_trace函数,其中对主要变化是对检索到的知识数量进行判断,如果知识数量满足要求则认为回答是充分的:

def ask_with_trace(self, question, chat_history=None):
    if chat_history is None:
        chat_history = []

    retrieved = self.retrieve(question, k=self.top_k)

    # rerank(用 text)
    texts = [c["text"] for c in retrieved]
    sorted_indices = self.rerank(question, texts)
    best_chunks = [retrieved[i] for i in sorted_indices[:self.rerank_k]]

    retrieved_chunks = []
    for c in best_chunks:
        retrieved_chunks.append({
            "source": c["source"],
            "text": c["text"],
        })

    # 最小版 context sufficiency 规则:
    # 当前先用 retrieved_chunks 数量判断,后续可以升级为相似度阈值 / rerank 分数 / Reflection 判断
    context_sufficient = len(retrieved_chunks) >= 2

    if not context_sufficient:
        return {
            "answer": (
                "当前知识库检索到的证据不足,不能基于现有论文片段做出可靠回答。"
                "建议补充更多相关论文,或换一种更具体的问题重新提问。"
            ),
            "retrieved_chunks": retrieved_chunks,
            "context_sufficient": False
        }

    # 拼 context(加来源)
    context = ""
    for c in best_chunks:
        context += f"[Source: {c['source']}]\n{c['text']}\n\n"

    messages = [
        {
            "role": "system",
            "content": (
                "You are a helpful assistant. "
                "Answer based on context and conversation history. "
                "If the context is not enough, clearly say the evidence is insufficient."
            )
        }
    ]

    # 保留历史对话
    messages.extend(chat_history)

    messages.append({
        "role": "user",
        "content": f"{context}\n\nQuestion: {question}"
    })

    response = client.chat.completions.create(
        model=CHAT_MODEL,
        messages=messages
    )

    answer = response.choices[0].message.content

    return {
        "answer": answer,
        "retrieved_chunks": retrieved_chunks,
        "context_sufficient": context_sufficient
    }

随后在问题生成节点做调整,返回状态图中context_sufficient信息:

def generate_answer_node(state: AgentState) -> AgentState:
    workflow_path = state.get("workflow_path", []) + ["generate_answer"]

    if state.get("error"):
        logger.warning(f"[generate_answer_node] error found in state: {state['error']}")
        return {
            "final_answer": f"系统执行过程中出现问题:{state['error']}",
            "retrieved_chunks": state.get("retrieved_chunks", []),
            "context_sufficient": state.get("context_sufficient"),
            "workflow_path": workflow_path
        }

    tool_result = state["tool_result"]
    logger.info(f"[generate_answer_node] final_answer: {tool_result['tool_output']}")

    output = tool_result["tool_output"]

    if isinstance(output, dict):
        return {
            "final_answer": output.get("answer", ""),
            "retrieved_chunks": output.get("retrieved_chunks", []),
            "context_sufficient": output.get("context_sufficient"),
            "workflow_path": workflow_path
        }

    return {
        "final_answer": str(output),
        "retrieved_chunks": [],
        "context_sufficient": state.get("context_sufficient"),
        "workflow_path": workflow_path
    }

# app/main.py中增加一个字段
agent_trace = {
    "route_decision": decision,
    "tool_used": tool_result.get("tool_name", ""),
    "tool_input": tool_result.get("tool_input", ""),
    "fallback_used": result.get("fallback_used", False),
    "context_sufficient": result.get("context_sufficient"),
    "error": result.get("error", ""),
    "retry_count": result.get("retry_count", 0),
    "workflow": result.get(
        "workflow_path",
        ["choose_tool", "execute_tool", "generate_answer"]
    )
}

2. P0 升级后的整体工作流

经过 P0-1 到 P0-3 后,当前 Agent Workflow 大致变成:

用户问题
→ choose_tool
→ 输出 tool + reason
→ execute_tool
→ 判断工具执行状态
→ 如果是 RAG,检查 context_sufficient
→ sufficient:generate_answer
→ insufficient 或工具异常:llm_fallback
→ 返回 final_answer + Agent Trace

相比之前,这个流程增加了三个关键能力:

  • reason:解释为什么选择某个工具;
  • fallback:工具异常或结果不足时有兜底路径;
  • context_sufficient:判断 RAG 上下文是否足够支撑回答。

3. 这次升级对 Agent Trace 的影响

前端 Agent Trace 不再只是展示调用了哪个工具,而是可以展示更多工作流状态,例如:

  • route
  • reason
  • fallback_used
  • context_sufficient

这样前端展示就不只是“问答结果”,而是能看到一次 Agent Workflow 的执行过程。

系统不是黑盒回答,而是会记录工具选择、选择原因、工具执行结果、上下文充分性和 fallback 状态。

4. 下一步计划

P0 升级完成后,下一步不是先做一轮工程细节收口:

  • 上传 PDF 后展示 reload_kb 结果;
  • 替换 calculator_tool 中的裸 eval;
  • 调整 chunk_size,让论文切分更符合语义粒度;
  • 检查聊天页面中 Conversation、Retrieved Context 和 Agent Trace 的 card 结构;
  • 同步 README 的项目表述。

如果这篇文章对你有帮助,可以点个赞~
完整代码地址:https://github.com/1186141415/Paper-RAG-Agent-with-LangGraph

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐