模块五-AI系统架构设计 | 第31讲:LLM 应用架构模式全景 - RAG、Agent、Chain、Tool Calling 的架构选型
本讲目标:建立 LLM 应用架构的「分类学」与选型框架;理解 Simple Chain、RAG、Agent、Tool Calling、Agentic RAG 的边界与组合方式;对照 CodeSentinel 贯穿项目,说明为何采用 Agentic RAG;用 LangChain(LCEL + Tool + Agent)给出每种模式的可运行最小示例;从延迟、成本、准确率、复杂度四维度做架构对比表。读完你应能向团队清晰陈述:何时不必上 Agent,何时必须混合检索与工具。
开场:别让「Agent 万能论」毁掉你的架构
过去两年,业界对 LLM 应用有一个常见误区:凡是智能助手都要做成 Agent,凡是企业知识都要做成 RAG,凡是集成外部系统都要上 Tool Calling。结果是:简单问答被拖进多轮循环,成本与延迟失控;本该结构化的规则审核被向量检索「猜答案」;本该一次函数调用的集成被模型反复试错。
架构模式的本质是约束求解:在给定任务类型、数据形态、合规要求与 SLA 下,选择最少的 moving parts 完成任务。Simple Chain 不是「落后」,而是许多场景的最优解;RAG 不是「搜索」,而是把外部知识以可控方式注入上下文;Agent 不是「更聪明」,而是把规划与执行显式化并承担失败代价;Tool Calling 是结构化 I/O 的契约层;Agentic RAG 则是当「知识密集 + 多步推理 + 外部动作」同时出现时,才值得支付的复杂度税。
CodeSentinel 作为 AI 驱动的代码审核与架构治理平台,典型输入是 PR diff、仓库元数据、策略规则与历史评审记录;典型输出是可定位的 finding、风险等级与可执行的整改建议。它既需要检索相似代码与历史案例(RAG),又需要按文件类型与变更范围选择不同分析工具(Tool Calling),还需要在多轮观察后决定是否扩大检索范围或请求更多上下文(Agent)。因此本讲将其定位为 Agentic RAG:不是名词堆砌,而是职责分离后的组合架构。
下面先给出模式分类的全局视图与对比流程图,再深入原理,最后用 LangChain 可运行代码把五种模式落到同一套接口风格上,便于你在团队内做 POC 与评审。
为了把抽象模式落到工程语言,你可以先回答三个问题:第一,事实从哪里来? 若事实应来自「当前仓库与历史变更」,就必须有检索或工具,而不是指望模型背诵。第二,决策是否需要分支? 若同一类 PR 在不同目录、不同语言、不同依赖版本下需要不同分析路径,就意味着存在动态策略,Agent 或「有限状态机 + Tool Calling」会优于单段提示词。第三,失败时如何降级? 线上系统必须定义:向量库不可用怎么办、工具超时会怎样、模型输出无法解析又如何。把降级路径画清楚,你会发现很多「看起来很酷」的 Agent 图,在真实故障场景里并没有退路,这正是架构评审要拦截的风险。
本讲刻意把 Tool Calling 与 Agent 分开讲解,因为在企业落地时它们对应不同的治理强度:Tool Calling 更像「带 schema 的函数分发」,适合放进严格的白名单与审计;Agent 更像「可循环的控制流」,需要更强的运行时约束。CodeSentinel 的推荐路径是:先用可观测的 Tool Calling 打通分析链路,再引入小步 Agent 做检索扩缩与策略选择,最后才把 RAG 与 Agent 合并为 Agentic RAG 的主形态。跳跃式建设往往导致你同时调试索引质量、工具稳定性与循环停止条件,排障成本呈乘法增长。
全局视角:五种模式在输入输出上的差异(Mermaid)
flowchart TB
subgraph SC[Simple Chain]
A1[用户输入] --> P1[Prompt 模板]
P1 --> L1[LLM]
L1 --> O1[文本输出]
end
subgraph RAG[RAG]
A2[用户查询] --> R2[Retriever 检索]
R2 --> C2[上下文拼装]
C2 --> L2[LLM 生成]
L2 --> O2[带引用输出]
end
subgraph AG[Agent 循环]
A3[任务目标] --> PL[规划/推理]
PL --> ACT[行动]
ACT --> OBS[观察环境反馈]
OBS --> PL
PL --> O3[最终结果]
end
subgraph TC[Tool Calling]
A4[用户请求] --> L4[LLM 决策]
L4 -->|tool_calls| T4[工具执行]
T4 --> L4b[LLM 汇总]
L4b --> O4[结构化结果]
end
subgraph AR[Agentic RAG]
A5[复杂任务] --> R5[RAG 注入知识]
R5 --> AG5[Agent 循环]
AG5 --> TC5[工具调用]
TC5 --> O5[可执行结论]
end
CodeSentinel:Agentic RAG 的职责切分(Mermaid)
flowchart LR
subgraph In[输入侧]
PR[PR / Diff]
REPO[仓库快照元数据]
POL[策略与门禁规则]
end
subgraph RAG_L[RAG 层]
IDX[代码块向量索引]
HIST[历史评审片段]
DOC[架构决策 ADR]
end
subgraph Agent_L[Agent 层]
PLAN[变更影响推理]
LOOP[扩缩检索范围]
STOP[终止条件判定]
end
subgraph Tools[Tool Calling 层]
AST[AST 静态分析]
CFG[依赖/调用图查询]
SBOM[组件清单扫描]
LLMCHK[规则模型二次判定]
end
PR --> IDX
PR --> PLAN
IDX --> PLAN
HIST --> PLAN
DOC --> PLAN
PLAN --> AST
PLAN --> CFG
AST --> LOOP
CFG --> LOOP
LOOP --> IDX
LOOP --> LLMCHK
LLMCHK --> STOP
STOP --> OUT[Findings + 评论草稿]
核心原理:分类学、边界与选型框架
1. Simple Chain:提示词 → 模型 → 答案
适用:输出格式稳定、知识基本在模型权重内、无需外部系统写操作。例如「把这段提交信息改写成符合 Conventional Commits 的说明」「把错误日志解释成可能根因列表(不要求引用仓库真实文件)」。
优点:延迟低、成本低、可测试性强(快照 prompt 即可回归)。风险:幻觉不可控;一旦任务需要「真实仓库状态」,必须把事实从外部注入,否则链再长也只是更会编的作家。
架构要点:把系统提示词、少样例(few-shot)、输出解析(JSON/XML)放在链上固定位置;用结构化输出 parser 降低后处理成本。
2. RAG:检索 → 增强 → 生成
适用:知识频繁变化、需要可追溯引用、或上下文远超模型窗口。代码审核里典型场景:检索「类似缺陷修复」「同类 API 误用案例」「团队自定义 lint 说明文档」。
优点:事实锚定、可解释(引用 chunk)。代价:索引质量决定上限;检索失败时模型仍会「硬答」,需要拒答策略与置信度门控。
架构要点:分块、元数据(路径、符号名、语言)、混合检索(向量 + 关键词)、重排序(本专栏后续讲)。RAG 不是替代 Agent,而是给 Agent 提供可验证的观察材料。
3. Agent:推理 → 行动 → 观察 循环
适用:任务步骤未知或需动态调整;需要多轮试错;环境可返回明确反馈(工具输出、检索结果、测试日志)。例如「先判断变更是否触及认证模块;若是,拉取相关测试与配置;若测试缺失,生成测试建议」。
优点:灵活。代价:延迟与费用呈循环倍数;需要清晰停止条件、最大步数、预算上限;否则会出现「反复检索同一查询」的路径依赖。
架构要点:把「规划」与「工具执行」解耦;记录 trajectory 便于审计;对生产系统必须加工具白名单与参数校验。
4. Tool Calling:LLM 决策调用哪个工具
适用:与外部系统交互(查询数据库、调用静态分析器、创建工单),且 I/O 适合 schema 化。它是 Agent 的工程化子集:你可以不用完整 ReAct 文案循环,仅一轮 tool call 就结束。
优点:比自由文本解析可靠;便于权限控制。风险:模型可能选错工具或填错参数,需要服务端校验与回退路径。
架构要点:工具描述(description)是「提示词工程」的一部分;返回结果要短而结构化,避免把巨量日志直接塞回模型。
5. Agentic RAG:Agent + RAG +(通常还有)Tool Calling
适用:同时满足:需要外部知识、需要多步策略、需要工具动作。CodeSentinel 即典型:RAG 提供「像什么历史问题」,Agent 决定「接下来查调用链还是查配置」,Tool Calling 落地「真实分析命令与查询」。
代价:系统复杂度与可观测性要求最高。必须有统一 traceId、逐步日志、以及「哪一步引入了哪段上下文」的归因。
选型决策框架(可贴墙)
- 是否需要外部知识且知识不在模型里? 是 → 引入 RAG;否 → 先考虑 Simple Chain。
- 是否需要多步推理与动态调整步骤? 是 → 引入 Agent;否 → RAG/Chain 可能足够。
- 是否需要对接可执行能力(分析器、平台 API)? 是 → Tool Calling(可嵌入 Agent 内)。
- 三者都要? → Agentic RAG,但必须分层:检索服务、工具网关、策略引擎、LLM 编排各司其职。
架构对比表(_latency / cost / accuracy / complexity)
| 模式 | 典型延迟 | 相对成本 | 准确率上限 | 工程复杂度 | 典型失败模式 |
|---|---|---|---|---|---|
| Simple Chain | 低 | 低 | 依赖模型与提示词 | 低 | 幻觉、不可追溯 |
| RAG | 中 | 中 | 受索引与检索影响 | 中 | 检索偏差、上下文污染 |
| Agent | 高 | 高 | 依赖停止条件与工具质量 | 高 | 循环浪费、策略漂移 |
| Tool Calling | 中 | 中 | 工具输出可靠则高 | 中 | 选错工具、参数错误 |
| Agentic RAG | 很高 | 很高 | 组合上限最高 | 很高 | 归因困难、成本失控 |
说明:准确率在代码领域建议拆成「事实正确(是否真有问题)」与「行动正确(建议是否可执行)」两项分别度量;单指标会误导决策。
再谈「复杂度税」:什么时候不值得上 Agent
Agent 的循环能力并不是免费午餐。每一次循环都意味着额外的模型调用、额外的工具副作用风险、以及额外的日志噪声。对于「变更范围小、规则明确、工具输出稳定」的审核任务,例如仅检测某几个正则模式或运行固定 linter,用 DAG 式流水线(确定性步骤)往往比 Agent 更可靠。Agent 更适合「变更影响面需要先探索后收敛」的任务,例如跨模块依赖、需要结合历史案例判断是否属于已知误报模式、或需要根据检索到的架构文档决定采用哪条治理规则。
另一个常被忽略的成本是人机协同。Simple Chain 与 RAG 的输出更容易被工程师快速校验:前者短,后者带来源。Agent 的长链路输出如果没有逐步展示,审查者会不信任;如果逐步展示,又可能泄露内部策略与工具细节。CodeSentinel 在产品层面通常需要「对外摘要 + 对内轨迹」双层结构:对外给 PR 评论的是可执行的结论与定位;对内给平台管理员的是完整 trajectory 与中间检索结果,用于复盘与调参。
与微服务拆分对齐:模式如何映射到边界上下文
若你的平台未来会演进到多租户、多语言、多规则包,建议把 LLM 相关能力拆成三个边界上下文:检索上下文负责索引、embedding、chunk 元数据;执行上下文负责工具、沙箱、扫描器编排;策略上下文负责规则版本、门禁、以及「是否允许 Agent 自动扩检索」。Simple Chain 主要落在策略上下文内的「文本生成子能力」;RAG 横跨检索与策略;Tool Calling 强依赖执行上下文;Agent 则是策略上下文里的编排引擎。这样映射的好处是:你可以单独对检索 SLA 做扩容,而不必把整个审核服务重部署。
代码实战:LangChain 五种模式可运行最小示例
以下示例使用 langchain-core 的 FakeListChatModel 模拟 LLM,无需 API Key 即可运行,便于 CI 与教学;将 build_demo_llm() 替换为真实 ChatOpenAI 即可对接生产。统一依赖:
langchain-core>=0.3.0
langchain-openai>=0.2.0
python-dotenv>=1.0.0
完整示例代码(patterns_demo.py)
"""
LLM 应用架构五种模式最小可运行示例(教学版)
运行: python patterns_demo.py
"""
from __future__ import annotations
import json
import os
from dataclasses import dataclass
from typing import Any, Callable, Dict, List
from langchain_core.language_models.fake import FakeListChatModel
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableLambda
from langchain_core.tools import tool
# ------------- LLM 工厂:教学用 Fake;生产换 ChatOpenAI -------------
def build_demo_llm() -> FakeListChatModel:
"""默认演示 LLM(仅用于连续调用 demo;各模式独立运行请用 build_fake_llm)。"""
return build_fake_llm(
[
"结论:该提交信息不符合 Conventional Commits,建议改为 feat(auth): add token refresh。",
(
"根据检索片段 [repo/security.md#oauth]:应在回调中校验 state;"
"建议在 `auth/callback.py` 增加常量时间比较。"
),
]
)
def build_fake_llm(responses: List[str]) -> FakeListChatModel:
return FakeListChatModel(responses=responses)
def build_production_llm():
"""生产环境示例:需要 OPENAI_API_KEY"""
try:
from langchain_openai import ChatOpenAI
except ImportError as e:
raise RuntimeError("安装 langchain-openai 并配置 OPENAI_API_KEY") from e
return ChatOpenAI(model=os.getenv("OPENAI_MODEL", "gpt-4o-mini"), temperature=0)
# ===================== 1) Simple Chain =====================
def run_simple_chain(llm) -> str:
prompt = ChatPromptTemplate.from_messages(
[
("system", "你是资深架构师,输出简洁中文结论。"),
("human", "请评审这条提交信息是否规范:{msg}"),
]
)
chain = prompt | llm | RunnableLambda(lambda m: m.content)
return str(chain.invoke({"msg": "fix stuff"}))
# ===================== 2) RAG:检索用占位,增强后生成 =====================
@dataclass
class Chunk:
id: str
text: str
def fake_retrieve(query: str) -> List[Chunk]:
_ = query
return [
Chunk("c1", "OAuth 回调必须校验 state,并避免将 access_token 打印到日志。"),
Chunk("c2", "Python 建议使用 secrets.compare_digest 做常量时间比较。"),
]
def run_rag_pattern(llm) -> str:
def retrieve_and_pack(inputs: Dict[str, Any]) -> Dict[str, Any]:
q = inputs["question"]
docs = fake_retrieve(q)
context = "\n".join(f"[{d.id}] {d.text}" for d in docs)
return {"question": q, "context": context}
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"你是代码安全审核助手。只能基于给定 context 回答;若不足请明确说「证据不足」。",
),
(
"human",
"问题:{question}\n\n检索上下文:\n{context}\n\n请给出修复建议。",
),
]
)
chain = RunnableLambda(retrieve_and_pack) | prompt | llm | RunnableLambda(lambda m: m.content)
return str(chain.invoke({"question": "OAuth 回调有哪些常见漏洞?"}))
# ===================== 3) Tool Calling(手动解析 Fake 的限制,展示契约)=====================
@tool
def run_static_scan(path: str) -> str:
"""对指定路径运行静态规则扫描,返回简短结果摘要。"""
return json.dumps({"path": path, "findings": ["可能的敏感日志字段: access_token"]})
def run_tool_calling_pattern(llm) -> str:
tools = [run_static_scan]
tool_map: Dict[str, Callable[..., str]] = {t.name: t.invoke for t in tools}
sys = SystemMessage(
content=(
"你是 CodeSentinel 工具编排器。你必须优先使用工具获取事实,再总结。"
"可用工具:run_static_scan(path: str)。"
)
)
human = HumanMessage(content="请检查 app/auth/callback.py 是否存在明显安全风险。")
ai: AIMessage = llm.invoke([sys, human])
# 真实 ChatOpenAI:ai.tool_calls 非空;FakeListChatModel:用 JSON 模拟
if getattr(ai, "tool_calls", None):
msgs = [sys, human, ai]
for call in ai.tool_calls:
name = call["name"]
args = call["args"]
tid = call["id"]
result = tool_map[name](args) if isinstance(args, dict) else tool_map[name](**args)
msgs.append(ToolMessage(content=str(result), tool_call_id=tid))
final = llm.invoke(msgs)
return str(final.content)
# Fake 路径:解析 content 中的 JSON(教学兼容)
try:
payload = json.loads(str(ai.content))
calls = payload.get("tool_calls") or []
except json.JSONDecodeError:
return str(ai.content)
msgs: List[Any] = [sys, human, ai]
for call in calls:
name = call["name"]
args = call.get("args", {})
tid = call.get("id", "tool_0")
result = tool_map[name](**args)
msgs.append(ToolMessage(content=str(result), tool_call_id=tid))
final = llm.invoke(msgs)
return str(final.content)
# ===================== 4) Agent(极简 ReAct 风格:两步规划-观察)=====================
def run_agent_loop_stub(llm) -> str:
"""教学版:不引入 langgraph 也能说明循环结构。"""
trajectory: List[str] = []
state = {"goal": "审核 auth 回调改动", "step": 0}
while state["step"] < 2:
plan = llm.invoke(
[
SystemMessage(content="你是 Agent,输出下一步行动要点(中文)。"),
HumanMessage(content=f"目标:{state['goal']},当前步:{state['step']}"),
]
)
trajectory.append(f"PLAN[{state['step']}]: {plan.content}")
obs = run_static_scan.invoke({"path": "app/auth/callback.py"})
trajectory.append(f"OBS[{state['step']}]: {obs}")
state["step"] += 1
summary = llm.invoke(
[
SystemMessage(content="根据 trajectory 输出最终审核摘要。"),
HumanMessage(content="\n".join(trajectory)),
]
)
return str(summary.content)
# ===================== 5) Agentic RAG:检索 + Agent 循环 + 工具 =====================
def run_agentic_rag(llm) -> str:
def rag_inject(inputs: Dict[str, Any]) -> Dict[str, Any]:
q = inputs["task"]
docs = fake_retrieve(q)
context = "\n".join(f"[{d.id}] {d.text}" for d in docs)
return {"task": q, "rag_context": context}
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"你是 CodeSentinel Agentic RAG 编排器。\n"
"先用 rag_context 作为证据,再决定是否调用工具 run_static_scan。\n"
"输出格式:\n"
"1) 证据要点\n2) 工具计划\n3) 预期观察\n",
),
("human", "任务:{task}\n\nRAG 证据:\n{rag_context}\n"),
]
)
plan_msg = (RunnableLambda(rag_inject) | prompt | llm | RunnableLambda(lambda m: m.content))
plan_text = str(plan_msg.invoke({"task": "评估 OAuth 回调改动的安全与一致性风险"}))
tool_out = run_static_scan.invoke({"path": "app/auth/callback.py"})
final_prompt = ChatPromptTemplate.from_messages(
[
("system", "综合「计划」与「工具结果」输出最终评审。"),
("human", "计划:\n{plan}\n\n工具结果:\n{tool}\n"),
]
)
final_chain = final_prompt | llm | RunnableLambda(lambda m: m.content)
return str(final_chain.invoke({"plan": plan_text, "tool": tool_out}))
def main() -> None:
print("=== 1) Simple Chain ===")
print(
run_simple_chain(
build_fake_llm(
["结论:该提交信息不符合 Conventional Commits,建议改为 feat(auth): add token refresh。"]
)
)
)
print("\n=== 2) RAG ===")
print(
run_rag_pattern(
build_fake_llm(
[
(
"根据检索片段 [repo/security.md#oauth]:应在回调中校验 state;"
"建议在 `auth/callback.py` 增加常量时间比较。"
)
]
)
)
)
print("\n=== 3) Tool Calling ===")
llm_tc = build_fake_llm(
[
json.dumps(
{
"tool_calls": [
{
"name": "run_static_scan",
"args": {"path": "app/auth/callback.py"},
"id": "call_1",
}
]
}
),
"工具结果显示存在明文日志风险;建议在记录前脱敏 token 字段。",
]
)
print(run_tool_calling_pattern(llm_tc))
print("\n=== 4) Agent (stub loop) ===")
llm_ag = build_fake_llm(
[
"下一步行动:调用工具 run_static_scan(path='app/auth/callback.py')",
"下一步行动:核对日志脱敏与 secrets.compare_digest 使用点",
"观察汇总:存在敏感日志风险;建议合并前修复。",
]
)
print(run_agent_loop_stub(llm_ag))
print("\n=== 5) Agentic RAG ===")
llm_ar = build_fake_llm(
[
"证据要点:state 校验与日志脱敏。\n工具计划:run_static_scan(app/auth/callback.py)\n预期观察:是否打印 token。",
"最终结论:结合工具结果,建议在日志层统一脱敏字段列表。",
]
)
print(run_agentic_rag(llm_ar))
if __name__ == "__main__":
main()
与 CodeSentinel 对齐的实现要点
- Simple Chain 适合「生成评论模板、改写措辞、生成 checklist」等低风险文本任务。
- RAG 独立为检索服务,返回带
file_path、symbol、commit、score的 chunk。 - Tool Calling 必须经过 Tool Gateway:鉴权、限流、路径白名单、超时、输出截断。
- Agent 建议用显式状态机或 LangGraph,把「扩检索」「问人类」「结束」做成节点。
- Agentic RAG 的关键是 归因:最终 finding 必须记录「哪些 chunk、哪些工具输出支撑结论」。
生产环境实战:从 POC 到可运维系统
1. 观测与预算
为每次评审生成 trace_id;记录每步 token、耗时、工具调用次数。对 Agent 设置 max_steps 与 max_tool_calls,超限则降级为「仅 RAG + 单次静态扫描」。
2. 安全
工具描述不要泄露内部主机名;扫描路径必须相对于仓库根并解析为真实路径后校验前缀,防止 ../../../etc/passwd 类穿越。
3. 评测
离线集:真实 PR + 人工标注 finding。对比「仅 Chain / 仅 RAG / Agentic RAG」的精确率与召回率,同时统计 误报成本(工程师每次误判的分钟数)。
4. 渐进演进
第一阶段:RAG + Tool Calling(无循环)。第二阶段:加入小步 Agent(最多两步)。第三阶段:全量 Agentic RAG,仅对高风险目录开启。
5. 多模型与多供应商策略
生产系统很少只绑定一个模型。常见做法是:路由层按任务类型选择「便宜小模型」或「强模型」:例如分类、摘要、格式化走小模型;涉及安全结论与复杂推理走大模型。对 CodeSentinel 而言,还可以在 Tool Calling 场景强制使用「更擅长遵循 JSON schema 的模型」,而 RAG 生成阶段使用「更长上下文窗口的模型」。无论怎么路由,都要保持同一 trace 内的模型版本、温度、top_p 等参数可追踪,否则线上问题无法复现。
6. 缓存与幂等:省成本,也省事故
对完全相同的 diff 片段与规则版本,缓存 LLM 输出可以显著降本;但要谨慎处理「缓存键」必须包含规则版本号、索引版本号、工具版本号,否则会出现「旧结论污染新策略」的事故。Tool Calling 侧更要强调幂等:同一个扫描任务重复触发不应产生副作用(例如重复创建工单)。把幂等键设计成 (repo, commit_sha, tool_name, normalized_args) 一类稳定字段,并在网关层统一拦截。
本讲小结(Mermaid mindmap)
mindmap
root((第31讲 LLM 架构模式))
Simple Chain
低延迟低成本
强提示词与结构化输出
RAG
可追溯引用
索引与检索质量是上限
Agent
动态步骤
需停止条件与预算
Tool Calling
Schema 化集成
工具网关与校验
Agentic RAG
CodeSentinel 主形态
归因与可观测性
选型框架
知识外部性
步骤不确定性
系统集成需求
思考题
- 你的业务里是否存在「用 Agent 但其实 Chain+RAG 足够」的过度设计?举一例说明如何降级。
- 若 RAG 检索到的 chunk 与工具扫描结果冲突,CodeSentinel 应以谁为准?请设计一条仲裁规则。
- 如何把「每次 tool call 的输入输出」纳入合规审计日志,同时避免泄露源码敏感片段?
下一讲预告
第32讲:向量数据库选型与实战,将围绕代码语义检索场景对比 Chroma、Milvus、Qdrant 的存储与索引差异,给出 CodeSentinel 的向量层设计与可运行索引示例,为 RAG 打下工程地基。
附录:模式选型速查流程图
flowchart TD
Q0[任务是否需要外部最新知识?] -->|否| C0[Simple Chain 是否足够?]
Q0 -->|是| R0[先上 RAG]
C0 -->|是| END1[采用 Simple Chain]
C0 -->|否| Q1[是否需要多步动态决策?]
R0 --> Q2[是否需要调用外部工具/平台 API?]
Q2 -->|否| END2[采用 RAG + 强提示词]
Q2 -->|是| Q1
Q1 -->|否| END3[采用 RAG + Tool Calling]
Q1 -->|是| END4[采用 Agentic RAG\n并加预算与归因]
延伸阅读:把模式落到「可验收的架构制品」上
当你向架构评审委员会解释 CodeSentinel 的 LLM 子系统时,建议准备四份制品,它们分别对应不同模式的工程化关注点。第一份是「数据流图(DFD)」:从 PR Webhook 到向量检索、再到工具网关与最终评论回写,标注每一步的 PII 与密钥暴露面。Simple Chain 场景下 DFD 最短;Agentic RAG 下必须标出循环边,并写明最大循环次数与熔断策略。第二份是「威胁模型」:Tool Calling 是最容易被忽视的放大器,任何可被模型触发的工具都要按「最小权限 + 参数 schema 校验 + 输出脱敏」三板斧处理;RAG 则要防止检索结果被投毒(恶意 PR 引入看似正常的注释诱导检索命中)。第三份是「SLO 与成本预算」:为每次评审定义 p95 延迟与单次最大费用;Agent 模式要把「每一步的平均 token」与「工具耗时」拆开监控,否则会出现模型很便宜、工具拖垮队列的反直觉现象。第四份是「离线评测集与回归门槛」:没有评测集的 Agent 上线等同于放任漂移;至少要有「高危漏洞应检出」「低噪注释不应误报」两类用例,并与仅 RAG、仅规则的基线对照。
从组织协作角度,模式选择还涉及「责任边界」。Simple Chain 往往由应用工程师单独维护即可;RAG 需要数据工程同学参与索引与清洗;Agent 需要平台工程提供队列、重试与可观测性;Tool Calling 需要安全团队审核工具清单。CodeSentinel 若一开始就把所有职责堆在「一个 FastAPI 文件里」,短期能 demo,长期必然返工。更稳妥的拆法是:编排层(Orchestrator)只负责状态机与调用顺序;检索层(Retriever Service)只负责返回带分数与元数据的 chunk;工具层(Tool Executor)只负责执行与审计;策略层(Policy Engine)决定哪些路径允许 Agent 自主扩缩。这样你可以在不影响核心审核规则的前提下,单独升级向量库或替换模型供应商。
最后补充三个在代码审核场景里极其常见的「反模式」,供你在评审同事方案时快速对齐语言。反模式一:把静态分析结果全文塞进 prompt,导致上下文被日志淹没,模型反而忽略 diff 本体;正确做法是让工具返回结构化摘要,并允许 Agent 二次请求「只展开某条 finding 的原文片段」。反模式二:检索与生成耦合成一个黑盒函数,出问题无法判断是 embedding 漂移还是 rerank 失效;正确做法是记录每步中间结果并支持回放。反模式三:无终止条件的自我反思(self-reflection),模型会不断「我再想想」;正确做法是显式终态与外部裁决(例如达到工具预算或检测到重复动作模式即停止)。把这些原则写进团队的 LLM 应用架构守则,比单纯讨论「用不用 Agent」更有长期价值。