part4对话agent方案设计与源码分析


前置准备——对话的需求、场景、价值分析

前面做的是一个rag,这里就是真正做一个agent了
一个根据知识,不确定地回答你的一个agent
背后用到的技术
1.RAG
2.prompt工程,提示词
3.多轮对话,记住上下文
4.流式输出,我们在这个让AI思考一个复杂问题的时候,是希望一下子蹦出一大段东西,还是说展现思考过程和慢慢地把答案吐出来?显然是后者
5.容错处理。也就是AI的幻觉方面,就像是我们常说的“老板的幻觉比AI还严重”。当agent在知识库里找不到答案时,会选择瞎编。但是正确的处理方式应该时说诚实告诉用户我暂时回答不了,或者换一个用户更能接受的委婉方法,而不是直接瞎编

架构设计:ReAct设计模式核心原理

ReAct:让AI边做边想。或者说一个,想,做,想的这么一个循环
大模型不善于计算,往往计算是调用工具来进行计算的

早期的ReAct是怎么实现的

早期就是解析成固定格式的字符串调用工具什么的方式

核心就是几行代码,本质上是一个while循环+字符串解析

现代ReAct是怎么实现的

Function Call(函数调用)

Function Call就是把古法ReAct标准化了–用json来统一工具的定义和调用格式。AI调用工具的时候不再输出自然语言,而是直接返回结构化的JSON

结果回传方面:工具的返回结果也按照规定的格式传回给AI

ReAct的优点

1.让AI具备了使用工具的能力

2.让AI的回答更准确、更可靠

3.具备了处理复杂多步骤任务的能力

架构设计:对话agent的核心流程解析

对话agent的核心目标是结合外部知识(RAG召回)与工具调用能力(ReAct模式),解决复杂问题

整体流程可概括为:

1.用户输入-embedding-向量数据库召回

2.构建带上下文(召回的内容)的prompt

3.ReAct模式多轮交互

4.最终输出答案

RAG召回:让agent学习外部知识

目标:从向量数据库中获取用户问题相关的上下文信息,避免大模型出现幻觉

步骤:

1.用户输入经IuputToRag Lambda Node处理,生成用于召回的字符串

2.调用Retriever组件(以Milvus数据库为例),通过Embedding将问题向量化

3.向量数据库执行相似度匹配,返回相关文档

4.结果存入map[“documents”],作为后续prompt的上下文来源

Prompt构建:动态拼接上下文与历史对话

目标:将用户输入、rag召回内容、对话历史整合成大模型可理解的prompt。prompt构建好后,将prompt移交给ReAct组件使用

ReAct模式:让Agent学会“思考-行动-观察”循环

目标:通过多轮工具调用解决复杂问题,核心是“显式思考-工具调用-结果观察”

关键组件深析

Lambda Node:数据流转的”转换器”:

  • InputToRag
    • 输入:用户原始问题(可自定义预处理,如过滤无关信息)。
    • 输出:用于RAG召回的字符串(直接影响召回精度,需确保与向量数据库存储内容匹配)。
  • InputToChat
    • 输入:用户问题+对话历史。
    • 输出:map结构(含 content / history 等key),作为 ChatTemplate 的动态参数来源。

Retriever:向量召回的 “连接器” 以 Milvus 实现为例,Retrieve 方法核心逻辑:

1
2
3
4
5
6
7
8
9
func (r *MilvusRetriever) Retrieve(ctx context.Context, input string) ([]*schema
// 1. 问题向量化
embedding, _ := r.embedding.Embed(ctx, input)
// 2. 向量数据库查询 (TopK相似度匹配)
results, _ := r.client.Search(ctx, embedding, 5) // 取Top5相关文档
// 3. 格式转换为schema.Document
return convertToDocuments(results), nil
}

Tool:Agent的“双手”

Tool本质是带描述的函数,需明确告诉大模型

  • 函数名称(如:“查询当前时间”)
  • 入参/返参格式(json来描述)
  • 使用场景(如“当问题设计当前时间时调用”)

源码分析

先把源码给出来看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
"""对话接口

提供基于 RAG Agent 的普通对话和流式对话接口
"""

import json
from fastapi import APIRouter, HTTPException
from sse_starlette.sse import EventSourceResponse
from app.models.request import ChatRequest, ClearRequest
from app.models.response import SessionInfoResponse, ApiResponse
from app.agent.mcp_client import format_exception_chain
from app.services.rag_agent_service import rag_agent_service
from loguru import logger

router = APIRouter()


@router.post("/chat")
async def chat(request: ChatRequest):
"""快速对话接口
{
"code": 200,
"message": "success",
"data": {
"success": true,
"answer": "回答内容",
"errorMessage": null
}
}

Args:
request: 对话请求

Returns:
统一格式的对话响应
"""
try:
logger.info(f"[会话 {request.id}] 收到快速对话请求: {request.question}")
answer = await rag_agent_service.query(
request.question,
session_id=request.id
)

logger.info(f"[会话 {request.id}] 快速对话完成")

return {
"code": 200,
"message": "success",
"data": {
"success": True,
"answer": answer,
"errorMessage": None
}
}

except Exception as e:
logger.error(f"对话接口错误: {e}")
return {
"code": 500,
"message": "error",
"data": {
"success": False,
"answer": None,
"errorMessage": str(e)
}
}


@router.post("/chat_stream")
async def chat_stream(request: ChatRequest):
"""流式对话接口(基于 RAG Agent,SSE)

返回 SSE 格式,data 字段为 JSON:

工具调用事件:
event: message
data: {"type":"tool_call","data":{"tool":"工具名","status":"start|end","input":{...}}}

内容流式事件:
event: message
data: {"type":"content","data":"内容块"}

完成事件:
event: message
data: {"type":"done","data":{"answer":"完整答案","tool_calls":[...]}}

Args:
request: 对话请求

Returns:
SSE 事件流
"""
logger.info(f"[会话 {request.id}] 收到流式对话请求: {request.question}")

async def event_generator():
try:
async for chunk in rag_agent_service.query_stream(request.question, session_id=request.id):
chunk_type = chunk.get("type", "unknown")
chunk_data = chunk.get("data", None)

# 处理调试类型消息(新增)
if chunk_type == "debug":
# 调试信息,可以选择发送或忽略
yield {
"event": "message",
"data": json.dumps({
"type": "debug",
"node": chunk.get("node", "unknown"),
"message_type": chunk.get("message_type", "unknown")
}, ensure_ascii=False)
}
elif chunk_type == "tool_call":
# 发送工具调用事件(可选,前端可以显示工具调用状态)
yield {
"event": "message",
"data": json.dumps({
"type": "tool_call",
"data": chunk_data
}, ensure_ascii=False)
}
elif chunk_type == "search_results":
# 发送检索结果(可选,前端可以忽略)
yield {
"event": "message",
"data": json.dumps({
"type": "search_results",
"data": chunk_data
}, ensure_ascii=False)
}
elif chunk_type == "content":
# 发送内容块 - 关键:data 必须是 JSON 字符串
yield {
"event": "message",
"data": json.dumps({
"type": "content",
"data": chunk_data
}, ensure_ascii=False)
}
elif chunk_type == "complete":
# 发送完成信号
yield {
"event": "message",
"data": json.dumps({
"type": "done",
"data": chunk_data
}, ensure_ascii=False)
}
elif chunk_type == "error":
# 发送错误信息
yield {
"event": "message",
"data": json.dumps({
"type": "error",
"data": str(chunk_data)
}, ensure_ascii=False)
}

logger.info(f"[会话 {request.id}] 流式对话完成")

except Exception as e:
logger.error(f"流式对话接口错误: {format_exception_chain(e)}")
yield {
"event": "message",
"data": json.dumps({
"type": "error",
"data": str(e)
}, ensure_ascii=False)
}

return EventSourceResponse(event_generator())


@router.post("/chat/clear", response_model=ApiResponse)
async def clear_session(request: ClearRequest):
"""清空会话历史

Args:
request: 清空请求

Returns:
操作结果
"""
try:
success = rag_agent_service.clear_session(request.session_id)
logger.info(f"清空会话: {request.session_id}, 结果: {success}")

return ApiResponse(
status="success" if success else "error",
message="会话已清空" if success else "清空会话失败",
data=None
)

except Exception as e:
logger.error(f"清空会话错误: {e}")
raise HTTPException(status_code=500, detail=str(e))


@router.get("/chat/session/{session_id}", response_model=SessionInfoResponse)
async def get_session_info(session_id: str) -> SessionInfoResponse:
"""查询会话历史

Args:
session_id: 会话 ID

Returns:
会话信息
"""
try:
history = rag_agent_service.get_session_history(session_id)

return SessionInfoResponse(
session_id=session_id,
message_count=len(history),
history=history
)

except Exception as e:
logger.error(f"获取会话信息错误: {e}")
raise HTTPException(status_code=500, detail=str(e))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
"""RAG Agent 服务 - 基于 LangGraph 的智能代理

使用 langchain_qwq 的 ChatQwen 原生集成,
支持真正的流式输出和更好的模型适配。
"""

from typing import Annotated, Any, AsyncGenerator, Dict, Sequence

from langchain.agents import create_agent
from langchain_core.messages import (
BaseMessage,
HumanMessage,
RemoveMessage,
SystemMessage,
)
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph.message import REMOVE_ALL_MESSAGES, add_messages
from loguru import logger
from typing_extensions import TypedDict
from langchain_qwq import ChatQwen

from app.config import config
from app.tools import DEFAULT_LOCAL_AGENT_TOOLS
from app.agent.mcp_client import (
get_mcp_client_with_retry,
load_mcp_tools_safe,
format_exception_chain,
suggest_mcp_transport,
)

# 阿里千问大模型和langchain集成参考: https://docs.langchain.com/oss/python/integrations/chat/qwen
# 注意:需要配置环境变量 DASHSCOPE_API_BASE=https://dashscope.aliyuncs.com/compatible-mode/v1 否则默认访问的是新加坡站点
# 同时也需要配置环境变量 DASHSCOPE_API_KEY=your_api_key


class AgentState(TypedDict):
"""Agent 状态"""
messages: Annotated[Sequence[BaseMessage], add_messages]


def trim_messages_middleware(state: AgentState) -> dict[str, Any] | None:
"""
修剪消息历史,只保留最近的几条消息以适应上下文窗口

策略:
- 保留第一条系统消息(System Message)
- 保留最近的 6 条消息(3 轮对话)
- 当消息少于等于 7 条时,不做修剪

Args:
state: Agent 状态

Returns:
包含修剪后消息的字典,如果无需修剪则返回 None
"""
messages = state["messages"]

# 如果消息数量较少,无需修剪
if len(messages) <= 7:
return None

# 提取第一条系统消息
first_msg = messages[0]

# 保留最近的 6 条消息(确保包含完整的对话轮次)
recent_messages = messages[-6:] if len(messages) % 2 == 0 else messages[-7:]

# 构建新的消息列表
new_messages = [first_msg] + list(recent_messages)

logger.debug(f"修剪消息历史: {len(messages)} -> {len(new_messages)} 条")

return {
"messages": [
RemoveMessage(id=REMOVE_ALL_MESSAGES),
*new_messages
]
}


class RagAgentService:
"""RAG Agent 服务 - 使用 LangGraph + ChatQwen 原生集成"""

def __init__(self, streaming: bool = True):
"""初始化 RAG Agent 服务

Args:
streaming: 是否启用流式输出,默认为 True
"""
self.model_name = config.rag_model
self.streaming = streaming
self.system_prompt = self._build_system_prompt()


self.model = ChatQwen(
model=self.model_name,
api_key=config.dashscope_api_key,
temperature=0.7,
streaming=streaming,
)

# 定义基础工具(与 AIOps Planner/Executor 使用同一套默认本地工具)
self.tools = list(DEFAULT_LOCAL_AGENT_TOOLS)

# MCP 客户端(延迟初始化,使用全局管理)
self.mcp_tools: list = []

# 创建内存检查点(用于会话管理)
self.checkpointer = MemorySaver()

# Agent 初始化(会在异步方法中完成)
self.agent = None
self._agent_initialized = False

logger.info(f"RAG Agent 服务初始化完成 (ChatQwen), model={self.model_name}, streaming={streaming}")

async def _initialize_agent(self):
"""异步初始化 Agent(包括 MCP 工具)"""
if self._agent_initialized:
return

for name, server in config.mcp_servers.items():
hint = suggest_mcp_transport(
str(server.get("url", "")),
str(server.get("transport", "")),
)
if hint:
logger.warning(f"MCP 配置 [{name}]: {hint}")

mcp_client = await get_mcp_client_with_retry()
mcp_tools, mcp_err = await load_mcp_tools_safe(mcp_client)
if mcp_err:
logger.warning(
f"MCP 工具加载失败,将仅使用本地工具继续运行:\n{mcp_err}"
)
self.mcp_tools = []
else:
self.mcp_tools = mcp_tools
logger.info(f"成功加载 {len(mcp_tools)} 个 MCP 工具")

all_tools = self.tools + self.mcp_tools

self.agent = create_agent(
self.model,
tools=all_tools,
checkpointer=self.checkpointer,
)

self._agent_initialized = True


if all_tools:
tool_names = [tool.name if hasattr(tool, "name") else str(tool) for tool in all_tools]
logger.info(f"可用工具列表: {', '.join(tool_names)}")

def _build_system_prompt(self) -> str:
"""
构建系统提示词

注意:LangChain 框架会自动将工具信息传递给 LLM,
因此系统提示词中无需列举具体的工具列表。

Returns:
str: 系统提示词
"""
from textwrap import dedent

return dedent("""
你是一个专业的AI助手,能够使用多种工具来帮助用户解决问题。

工作原则:
1. 理解用户需求,选择合适的工具来完成任务
2. 当需要获取实时信息或专业知识时,主动使用相关工具
3. 基于工具返回的结果提供准确、专业的回答
4. 如果工具无法提供足够信息,请诚实地告知用户

回答要求:
- 保持友好、专业的语气
- 回答简洁明了,重点突出
- 基于事实,不编造信息
- 如有不确定的地方,明确说明

请根据用户的问题,灵活使用可用工具,提供高质量的帮助。
""").strip()

async def query(
self,
question: str,
session_id: str,
) -> str:
"""
非流式处理用户问题(一次性返回完整答案)

Args:
question: 用户问题
session_id: 会话ID(作为 thread_id)

Returns:
str: 完整答案
"""
try:
await self._initialize_agent()

logger.info(f"[会话 {session_id}] RAG Agent 收到查询(非流式): {question}")

# 构建消息列表(系统提示 + 用户问题)
messages = [
SystemMessage(content=self.system_prompt),
HumanMessage(content=question)
]

# 构建 Agent 输入
agent_input = {"messages": messages}

# 配置 thread_id(用于会话持久化)
config_dict = {
"configurable": {
"thread_id": session_id
}
}

result = await self.agent.ainvoke(
input=agent_input,
config=config_dict,
)

# 提取最终答案
messages_result = result.get("messages", [])
if messages_result:
last_message = messages_result[-1]
answer = last_message.content if hasattr(last_message, 'content') else str(last_message)

# 记录工具调用
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
tool_names = [tc.get("name", "unknown") for tc in last_message.tool_calls]
logger.info(f"[会话 {session_id}] Agent 调用了工具: {tool_names}")

logger.info(f"[会话 {session_id}] RAG Agent 查询完成(非流式)")
return answer

logger.warning(f"[会话 {session_id}] Agent 返回结果为空")
return ""

except Exception as e:
logger.error(
f"[会话 {session_id}] RAG Agent 查询失败(非流式): "
f"{format_exception_chain(e)}"
)
raise

async def query_stream(
self,
question: str,
session_id: str,
) -> AsyncGenerator[Dict[str, Any], None]:
"""
流式处理用户问题(逐步返回答案片段)

Args:
question: 用户问题
session_id: 会话ID(作为 thread_id)

Yields:
Dict[str, Any]: 包含流式数据的字典
- type: "content" | "tool_call" | "complete" | "error"
- data: 具体内容
"""
try:
await self._initialize_agent()

logger.info(f"[会话 {session_id}] RAG Agent 收到查询(流式): {question}")

# 构建消息列表(系统提示 + 用户问题)
messages = [
SystemMessage(content=self.system_prompt),
HumanMessage(content=question)
]

# 构建 Agent 输入
agent_input = {"messages": messages}

# 配置 thread_id(用于会话持久化)
config_dict = {
"configurable": {
"thread_id": session_id
}
}

async for token, metadata in self.agent.astream(
input=agent_input,
config=config_dict,
stream_mode="messages",
):
node_name = metadata.get('langgraph_node', 'unknown') if isinstance(metadata, dict) else 'unknown'
message_type = type(token).__name__

if message_type in ("AIMessage", "AIMessageChunk"):
content_blocks = getattr(token, 'content_blocks', None)

if content_blocks and isinstance(content_blocks, list):
for block in content_blocks:
if isinstance(block, dict) and block.get('type') == 'text':
text_content = block.get('text', '')
if text_content:
yield {
"type": "content",
"data": text_content,
"node": node_name
}

logger.info(f"[会话 {session_id}] RAG Agent 查询完成(流式)")
yield {"type": "complete"}

except Exception as e:
detail = format_exception_chain(e)
logger.error(
f"[会话 {session_id}] RAG Agent 查询失败(流式): {detail}"
)
yield {"type": "error", "data": detail}

def get_session_history(self, session_id: str) -> list:
"""
获取会话历史(从 MemorySaver checkpointer 中读取)

Args:
session_id: 会话ID(即 thread_id)

Returns:
list: 消息历史列表 [{"role": "user|assistant", "content": "...", "timestamp": "..."}]
"""
try:
# 使用 checkpointer 的 get 方法获取最新的检查点
config = {"configurable": {"thread_id": session_id}}

# 获取该 thread 的最新检查点
checkpoint_tuple = self.checkpointer.get(config)

if not checkpoint_tuple:
logger.info(f"获取会话历史: {session_id}, 消息数量: 0")
return []

# checkpoint_tuple 可能是命名元组或普通元组,安全地提取 checkpoint
# 通常第一个元素是 checkpoint 数据
if hasattr(checkpoint_tuple, 'checkpoint'):
checkpoint_data = checkpoint_tuple.checkpoint # type: ignore
else:
# 如果是普通元组,第一个元素是 checkpoint
checkpoint_data = checkpoint_tuple[0] if checkpoint_tuple else {}

# 从检查点中提取消息
messages = checkpoint_data.get("channel_values", {}).get("messages", [])

# 转换为前端需要的格式
history = []
for msg in messages:
# 跳过系统消息
if isinstance(msg, SystemMessage):
continue

role = "user" if isinstance(msg, HumanMessage) else "assistant"
content = msg.content if hasattr(msg, 'content') else str(msg)

# 提取时间戳(如果有的话)
timestamp = getattr(msg, 'timestamp', None)
if timestamp:
history.append({
"role": role,
"content": content,
"timestamp": timestamp
})
else:
from datetime import datetime
history.append({
"role": role,
"content": content,
"timestamp": datetime.now().isoformat()
})

logger.info(f"获取会话历史: {session_id}, 消息数量: {len(history)}")
return history

except Exception as e:
logger.error(f"获取会话历史失败: {session_id}, 错误: {e}")
return []

def clear_session(self, session_id: str) -> bool:
"""
清空会话历史(从 MemorySaver checkpointer 中删除)

Args:
session_id: 会话ID(即 thread_id)

Returns:
bool: 是否成功
"""
try:
# 使用 checkpointer 的 delete_thread 方法删除该 thread 的所有检查点
self.checkpointer.delete_thread(session_id)

logger.info(f"已清除会话历史: {session_id}")
return True

except Exception as e:
logger.error(f"清空会话历史失败: {session_id}, 错误: {e}")
return False

async def cleanup(self):
"""清理资源"""
try:
logger.info("清理 RAG Agent 服务资源...")
# MCP 客户端由全局管理器统一管理,无需手动清理
logger.info("RAG Agent 服务资源已清理")
except Exception as e:
logger.error(f"清理资源失败: {e}")


# 全局单例 - 启用流式输出
rag_agent_service = RagAgentService(streaming=True)

流程梳理

对话Agent的核心目标时结合外部知识(RAG召回)与工具调用能力(ReAct模式),解决复杂问题

整体流程可以概括为:

1.用户输入-embedding-向量数据库召回

2.将召回内容作为上下文注入prompt

3.LangGraph ReAct模式多轮工具调用

4.流式输出最终答案

实战

消息召回:

召回通过 retrieve_knowledge 工具实现,Agent 在推理时会自动判断是否需要调用该工具检索知识库。工具内部通过 VectorStoreManager.similarity_search 完成向量检索,详见 RAG 召回章节

1
2
3
# retrieve_knowledge 工具挂载到 Agent 上
self.tools = [retrieve_knowledge, get_current_time]

构建prompt

系统提示词在_build_system_prompt中构建,描述Agent的角色定位和行为准则。与工具序列无关–LangChain框架会自动将工具信息传递给大模型,prompt中无需手动列举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def _build_system_prompt(self) -> str:
from textwrap import dedent
return dedent("""
你是一个专业的AI助手,能够使用多种工具来帮助用户解决问题。

工作原则:
1. 理解用户需求,选择合适的工具来完成任务
2. 当需要获取实时信息或专业知识时,主动使用相关工具
3. 基于工具返回的结果提供准确、专业的回答
4. 如果工具无法提供足够信息,请诚实地告知用户

回答要求:
- 保持友好、专业的语气
- 回答简洁明了,重点突出
- 基于事实,不编造信息
- 如有不确定的地方,明确说明

请根据用户的问题,灵活使用可用工具,提供高质量的帮助。
""").strip()

历史会话由LangGraph的MemorySaver checkpointer自动管理,每次调用时传入相同的thread_id(即session_id)即可自动携带上下文,无需手动拼接历史消息到prompt

创建ReAct Agent

使用LangChain的Create_agent创建Agent,绑定ChatQwen模型、工具列表和MemorySaver检查点。MCP工具(腾讯云CLS日志、监控告警登)在首次请求时异步加载,与本地工具合并后一起绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class RagAgentService:
def __init__(self, streaming: bool = True):
self.model = ChatQwen(
model=config.rag_model, # 默认 qwen‑max
api_key=config.dashscope_api_key,
temperature=0.7,
streaming=streaming,
)

# 本地工具: RAG 知识检索 + 时间查询
self.tools = [retrieve_knowledge, get_current_time]

# 会话持久化 (基于内存的 checkpointer)
self.checkpointer = MemorySaver()

self.agent = None # 延迟初始化 (等待 MCP 工具加载完成)

async def _initialize_agent(self):
"""异步初始化 Agent(包括 MCP 工具)"""
if self._agent_initialized:
return

# 加载 MCP 工具 (CLS 日志服务 + 监控告警)
mcp_client = await get_mcp_client_with_retry()
mcp_tools = await mcp_client.get_tools()

# 合并所有工具
all_tools = self.tools + mcp_tools

self.agent = create_agent(
self.model,
tools=all_tools,
checkpointer=self.checkpointer,
)
self._agent_initialized = True

执行ReAct Agent

非流式调用

调用agent.ainvoke,等待Agent完成全部推理和工具调用后一次性返回结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async def query(self, question: str, session_id: str) -> str:
await self._initialize_agent()

messages = [
SystemMessage(content=self.system_prompt),
HumanMessage(content=question)
]

result = await self.agent.ainvoke(
input={"messages": messages},
config={"configurable": {"thread_id": session_id}},
)

# 取最后一条消息作为最终答案
last_message = result["messages"][-1]
return last_message.content

流式调用:

调用agent.astream,使用stream_mode=”messages”逐token输出,配合FastAPI的SSE接口实时推送给前端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
async def query_stream(self, question: str, session_id: str) -> AsyncGenerator:
await self._initialize_agent()

messages = [
SystemMessage(content=self.system_prompt),
HumanMessage(content=question)
]

async for token, metadata in self.agent.astream(
input={"messages": messages},
config={"configurable": {"thread_id": session_id}},
stream_mode="messages",
):
if type(token).__name__ in ("AIMessage", "AIMessageChunk"):
content_blocks = getattr(token, 'content_blocks', None)
if content_blocks:
for block in content_blocks:
if isinstance(block, dict) and block.get('type') == 'text':
text = block.get('text', '')
if text:
yield {"type": "content", "data": text}
yield {"type": "complete"}

SSE接口层

chat_stream接口将query_stream产生的时间包装成SSE格式推送给客户端,不同类型的事件对应不同的前端展示逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@router.post("/chat_stream")
async def chat_stream(request: ChatRequest):
async def event_generator():
async for chunk in rag_agent_service.query_stream(request.question, sess):
chunk_type = chunk.get("type")

if chunk_type == "content":
# 逐 token 文本内容
yield {
"event": "message",
"data": json.dumps({"type": "content", "data": chunk["data"]})
}
elif chunk_type == "tool_call":
# 工具调用状态(前端可展示"正在检索知识库…"等提示)
yield {
"event": "message",
"data": json.dumps({"type": "tool_call", "data": chunk["data"]})
}
elif chunk_type == "complete":
# 完成信号
yield {
"event": "message",
"data": json.dumps({"type": "done", "data": chunk.get("data")})
}
elif chunk_type == "error":
yield {
"event": "message",
"data": json.dumps({"type": "error", "data": str(chunk["data"])})
}
return EventSourceResponse(event_generator())

crul调用实例:

1
2
3
4
5
6
7
8
9
10
# 非流式对话
curl -X POST http://localhost:8000/api/chat \
-H "Content-Type: application/json" \
-d '{"id": "session-001", "question": "CPU 使用率过高怎么排查?"}'

# 流式对话 (SSE)
curl -X POST http://localhost:8000/api/chat_stream \
-H "Content-Type: application/json" \
-d '{"id": "session-001", "question": "CPU 使用率过高怎么排查?"}'

总结

至此,对话 Agent 的核心流程 ——RAG 召回与 ReAct 模式的代码就讲完了。框架帮我们做了很多事情:LangChain 负责工具绑定与调用,LangGraph 负责多轮推理的状态流转,MemorySaver 负责会话历史管理。核心是要搞懂设计原理:RAG 补充外部知识,ReAct 让模型具备多步骤工具调用能力

源码分析:API接口与Agent的整合

快速对话接口:

与大模型对话,相同ID的对话带有上下文记忆功能

请求方式:POST /api/chat

请求字段

字段名 类型 描述
Id string 对话的唯一标识
Question string 用户提问

响应字段

字段名 类型 描述
Answer string 系统回答

示例,就有点像抓数据包发过去的这样一个逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 示例: 快速对话
curl -X POST http://localhost:6872/api/chat \
-H "Content-Type: application/json" \
-d '{
"Id": "session-001",
"Question": "什么是人工智能?"
}'

# 响应
{
"message": "OK",
"data": {
"answer": "AI 的回答内容..."
}
}

流式对话接口:

与大模型对话,相同ID的对话带有上下文记忆功能,通过SSE实现流式输出回答

请求方法:POST /api/chat_stream

请求字段

字段名 类型 描述
Id string 对话的唯一标识
Question string 用户提问

响应字段

字段名 类型 描述

SSE响应格式:

event 类型 含义
connected 代表连接建立成功
message 回复的文本片段,会多次发送
error 连接异常,断开连接
done 消息推送完毕,断开连接

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 示例: 流式对话
curl -X POST http://localhost:6872/api/chat_stream \
-H "Content-Type: application/json" \
-d '{
"Id": "session-001",
"Question": "什么是人工智能?"
}'

# 响应(SSE事件流)
id: <timestamp>
event: connected
data: {"status": "connected", "client_id": "session-001"}

id: <timestamp>
event: message
data: 人工智能(AI)

id: <timestamp>
event: message
data: 的发展历史

id: <timestamp>
event: message
data: 可以追溯到...

id: <timestamp>
event: done
data: Stream completed

快速对话接口的核心实现

  1. 接收请求,取出 id(session_id)和 question
  2. 调用 rag_agent_service.query 执行 Agent 推理,thread_idsession_id
  3. LangGraph MemorySaver 自动完成历史消息的读取与写入,无需手动管理
  4. 返回答案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@router.post("/chat")
async def chat(request: ChatRequest):
logger.info(f"[会话 {request.id}] 收到快速对话请求: {request.question}")

# 直接调用 Agent,thread_id 决定会话隔离,历史消息由 MemorySaver 自动维护
answer = await rag_agent_service.query(
request.question,
session_id=request.id
)

return {
"code": 200,
"message": "success",
"data": {
"success": True,
"answer": answer,
"errorMessage": None
}
}

query 方法内部将系统提示 + 用户问题包装成消息列表,通过 agent.ainvoke 执行完整的 ReAct 推理链,并从最后一条消息中取出答案。thread_idMemorySaver 配合,让相同 id 的请求自动携带历史上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async def query(self, question: str, session_id: str) -> str:
await self._initialize_agent()

messages = [
SystemMessage(content=self.system_prompt),
HumanMessage(content=question)
]

# thread_id 相同则自动读取 MemorySaver 中的历史消息
result = await self.agent.ainvoke(
input={"messages": messages},
config={"configurable": {"thread_id": session_id}},
)

# 取最后一条消息作为最终答案
last_message = result["messages"][-1]
return last_message.content

会话历史的消息裁剪由 trim_messages_middleware 节点负责,策略是保留第一条系统消息 + 最近 6 条消息(约 3 轮对话),防止多轮对话超出大模型的上下文窗口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def trim_messages_middleware(state: AgentState):
messages = state["messages"]
if len(messages) <= 7:
return None # 消息较少,无需裁剪

first_msg = messages[0] # 保留系统消息
recent_messages = messages[-6:] if len(messages) % 2 == 0 else messages[-7:]

return {
"messages": [
RemoveMessage(id=REMOVE_ALL_MESSAGES), # 清空所有旧消息
*([first_msg] + list(recent_messages)) # 写入保留的消息
]
}

流式对话接口的核心实现

SSE返回的消息event类型:

event 类型 含义
message (type=content) 回复的文本片段,会多次发送
message (type=tool_call) 工具调用状态通知
message (type=done) 消息推送完毕
message (type=error) 发生异常
  1. 流式对话的核心是 SSE,FastAPI 通过 EventSourceResponse 实现,无需手动设置 HTTP 头
  2. Agent 使用 agent.astreamstream_mode="messages" 模式,逐 token 产生输出
  3. 每次从流中读到文本内容,就通过 SSE 发送给客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@router.post("/chat_stream")
async def chat_stream(request: ChatRequest):
async def event_generator():
async for chunk in rag_agent_service.query_stream(
request.question, session_id=request.id
):
chunk_type = chunk.get("type")

if chunk_type == "content":
# 逐 token 文本片段,实时推送
yield {
"event": "message",
"data": json.dumps(
{"type": "content", "data": chunk["data"]},
ensure_ascii=False
)
}
elif chunk_type == "tool_call":
# 工具调用状态(前端可展示"正在检索知识库..."等提示)
yield {
"event": "message",
"data": json.dumps(
{"type": "tool_call", "data": chunk.get("data")},
ensure_ascii=False
)
}
elif chunk_type == "complete":
# 推送完成信号
yield {
"event": "message",
"data": json.dumps({"type": "done", "data": None}, ensure_ascii=False)
}
elif chunk_type == "error":
yield {
"event": "message",
"data": json.dumps(
{"type": "error", "data": str(chunk.get("data"))},
ensure_ascii=False
)
}
# EventSourceResponse 自动处理 SSE 协议头和连接管理
return EventSourceResponse(event_generator())

query_stream 方法使用 agent.astreamstream_mode="messages" 模式,每个 token 触发一次回调,从 content_blocks 中提取文本块后 yield 给上层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
async def query_stream(self, question: str, session_id: str) -> AsyncGenerator:
await self._initialize_agent()

messages = [
SystemMessage(content=self.system_prompt),
HumanMessage(content=question)
]

async for token, metadata in self.agent.astream(
input={"messages": messages},
config={"configurable": {"thread_id": session_id}},
stream_mode="messages", # 逐 token 输出模式
):
if type(token).__name__ in ("AIMessage", "AIMessageChunk"):
content_blocks = getattr(token, 'content_blocks', None)
if content_blocks:
for block in content_blocks:
if isinstance(block, dict) and block.get('type') == 'text':
text = block.get('text', '')
if text:
yield {"type": "content", "data": text}
yield {"type": "complete"}


文章作者: wuk0Ng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 wuk0Ng !
评论
  目录