9、实现真正的ReAct智能体!!!
我们来看以下专业的回答:
ReAct(Reasoning + Acting)是一种结合推理(Reasoning)与行动(Acting)的智能体(Agent)框架,最初由 Google Research 和 Princeton University 的研究人员在 2022 年提出(论文:ReAct: Synergizing Reasoning and Acting in Language Models)。
核心思想:
ReAct 的核心理念是:让语言模型在解决问题时,交替进行推理(思考)和行动(调用工具或获取信息) ,而不是一次性生成答案。这种方式模仿人类解决问题的过程——边思考、边行动、再根据反馈调整策略。
ReAct 的工作流程
在一个典型的 ReAct 循环中,智能体会交替输出两种类型的步骤:
-
Thought(思考) :分析当前状态、制定计划、推理下一步该做什么。
-
Action(行动)
:执行一个具体操作,例如:
- 查询搜索引擎
- 调用 API
- 读取文档
- 操作环境(如在游戏或模拟器中)
然后,环境会返回一个 Observation(观察) ,智能体再基于这个观察继续思考和行动,直到达成目标。
示例(问答场景):
问题:谁是《百年孤独》的作者,他哪年获得诺贝尔文学奖?
智能体可能这样执行 ReAct 循环:
- Thought: 我需要先找出《百年孤独》的作者。
- Action: 搜索("百年孤独 作者")
- Observation: 加夫列尔·加西亚·马尔克斯
- Thought: 现在我知道作者是马尔克斯,接下来查他获得诺贝尔文学奖的年份。
- Action: 搜索("加西亚·马尔克斯 诺贝尔文学奖 年份")
- Observation: 1982年
- Thought: 我已获得全部信息,可以回答问题。
- Answer: 《百年孤独》的作者是加夫列尔·加西亚·马尔克斯,他于1982年获得诺贝尔文学奖。
类似于我们平时所使用的llm的深度思考功能对吧,
所以我们的目标就是让llm进行“思考”,并做出“行动”,也就是调用工具。
我们先从第一个方面进行优化,也就是“思考”。怎么能让llm进行深度的思考或者说是推理呢,一方面是取决于llm本身的质量,另一方面就是要依靠提示词!
我们可以看一下目前所写的提示词,非常简单,对吧,所以我们就在提示词这方面优化,
现在修改原来的提示词,在ai_client.py中添加如下系统提示词:
system_prompt = """你是一名资深 Java 开发 Agent,精通 JDK 8-17、Maven/Gradle、Spring 生态(Spring Boot、Spring MVC、Spring Data JPA、Spring Cloud)、并发与性能调优、JVM 诊断、微服务工程实践。你的目标是在尽可能少的步数内,为用户提供专业、可直接运行的 Java 方案(代码、命令、配置与解释)。
必须遵循:
1. 只使用下方列出的工具;确需外部信息优先用知识库检索,其次再搜索网络。
2. 每次只能使用一个工具;用完等待 Observation 再决定下一步。
3. 回答必须自洽、可执行:给出完整代码需包含必要 import、pom.xml/gradle 配置要给出关键依赖,命令附上执行目录与前置条件。
4. 优先中文回答;代码用合适语言高亮,例如 ```java、```xml、```bash。
5. 安全与最小影响:涉及 shell/文件操作要注明作用与风险,谨慎执行写入/覆盖类操作。
6. 如需求不清,先用 1-3 句澄清关键约束(JDK 版本、构建工具、框架版本、运行环境等)。
Java 解决方案要求:
- 代码质量:命名清晰、边界条件、异常处理、日志与注释(只解释“为什么”)。
- 并发与性能:合理使用线程池、CompletableFuture、锁与无锁结构;避免阻塞;给出复杂度与潜在瓶颈;必要时提供 JMH/压测建议。
- Spring 规范:分层清晰(controller/service/repository)、DTO/VO 转换、事务传播与隔离级别、校验与全局异常处理。
- 数据访问:JPA/Query 方法/Specification/原生 SQL 的取舍;连接池与 N+1 问题规避;分页与索引建议。
- 构建与运行:提供 Maven/Gradle 脚本或命令;测试用例(JUnit5/MockMVC/Mockito)示例;Docker/容器化要点(如需要)。
- JVM 与运维:必要时给出 GC、内存、线程与诊断命令(jcmd/jmap/jstack)、常见参数与可观测性建议。
可用工具:
{tools}
工具使用原则:
- knowledge_search:优先检索本地知识库(如项目内 `doc/` 的最佳实践、调优笔记),再综合回答。
- search_web / crawl_url:仅当知识库不足时使用,并核对来源可靠性。
- read_from_file / write_to_file:仅当用户明确要求“保存/导出/写入/生成文件”时才使用;否则不要调用。写入前在回答里说明将要写入的路径与用途。
- run_shell_command:仅在需要验证构建/测试/脚手架时使用,先说明命令作用与期望输出。
输出格式(ReAct):
格式要求:Action Input 必须是严格 JSON 对象,不要使用代码块或额外文本。
# 输出必须严格遵循以下格式,不要有任何额外内容、解释或 Markdown:
Question: 用户的问题
Thought: 我是否需要使用工具?(是/否)
Action: 工具名称(必须是 [{tool_names}] 之一,若不需要工具则跳过 Action 和 Action Input)
Action Input: {{ "query": "关键词" }}
Observation: 工具返回结果
...(可重复)
Thought: 我现在知道答案了。
Final Answer: 简明回答,包含代码/配置/命令(如需要)
# 注意:
- 如果不需要工具,直接输出 Final Answer,不要写 Action。
- Action Input 必须是合法 JSON,不要用代码块。
- 每个字段独占一行,以字段名开头(如 "Thought:")。
"""
可以看到,当前这个新的系统提示词看起来更详细更高级了同时呢也加了工具调用相关的提示词,这也是遵循了ReAct模式,
然后修改提示词模板,使用我们新的系统提示词
# 构建 Prompt
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{input}"),
("human", "{agent_scratchpad}")
])
补充:
gent_scratchpad是 LangChain 中 Agent(智能体)用于记录中间推理步骤的临时工作区,它的核心作用是:保存 Agent 在当前轮次中已经执行的 Thought → Action → Observation 循环的历史,供下一步决策使用。
通俗理解
想象你在解一道复杂题:
- 你先想:“我需要查资料” → 这是
Thought- 你去查维基百科 → 这是
Action- 你看到结果:“Java 线程池有 corePoolSize...” → 这是
Observation- 你再想:“现在我知道了,可以回答了” → 新的
Thought这个 “思考 → 行动 → 观察” 的过程记录,就是
agent_scratchpad。Agent 每次调用 LLM 时,都会把这段记录拼接到 prompt 里,让模型知道:“我已经做了这些,接下来该干嘛?”
但是这样就能成功运行吗,答案当然是不能运行,为什么呢,一方面就是我们还没有开发相关的工具,另一方面就是我们目前所使用的这种构建链的方式,是不支持工具调用的。要想使用ReAct模式,LangChain提供了Agent模式,或者说是代理。
来看一下官方文档的解释:
代理的核心思想是使用LLM来选择要采取的一系列动作。 在链式结构中,一系列动作是硬编码的(在代码中)。 在代理中,使用语言模型作为推理引擎来确定要采取的动作及其顺序。
这里有几个关键组件:
- 代理
这是负责决定下一步采取什么动作的类。 这是由语言模型和提示驱动的。 该提示可以包括以下内容:
- 代理的个性(对于以某种方式响应很有用)
- 代理的背景上下文(对于给予其更多关于所要求完成的任务类型的上下文很有用)
- 调用更好推理的提示策略(最著名/广泛使用的是ReAct)
LangChain提供了几种不同类型的代理来入门。 即使如此,您可能还希望使用部分(1)和(2)自定义这些代理。 有关代理类型的完整列表,请参见代理类型
- 工具
工具是代理调用的函数。 这里有两个重要的考虑因素:
- 给代理访问正确工具的权限
- 以对代理最有帮助的方式描述工具
如果没有这两者,您想要构建的代理将无法工作。 如果您不给代理访问正确工具的权限,它将永远无法完成目标。 如果您不正确描述工具,代理将不知道如何正确使用它们。
LangChain提供了一系列广泛的工具来入门,同时也可以轻松定义自己的工具(包括自定义描述)。 有关工具的完整列表,请参见这里
- 工具包
代理可以访问的工具集合通常比单个工具更重要。 为此,LangChain提供了工具包的概念-用于实现特定目标所需的一组工具。 通常一个工具包中有3-5个工具。
LangChain提供了一系列广泛的工具包来入门。 有关工具包的完整列表,请参见这里
- 代理执行器
代理执行器是代理的运行时。 这是实际调用代理并执行其选择的动作的部分。 以下是此运行时的伪代码:
next_action = agent.get_action(...) while next_action != AgentFinish: observation = run(next_action) next_action = agent.get_action(..., next_action, observation) return next_action虽然这看起来很简单,但此运行时为您处理了几个复杂性,包括:
- 处理代理选择不存在的工具的情况
- 处理工具发生错误的情况
- 处理代理生成无法解析为工具调用的输出的情况
- 在所有级别上记录和可观察性(代理决策,工具调用)-可以输出到stdout或LangSmith
在LangChain中from langchain.agents 中提供了create_react_agent创建代理,AgentExecutor代理执行器的类,
9.1 重构LangChain链
下面来改造代码,完全重构掉LangChain链。
首先导入所需要的包并创建一个代理
from langchain.agents import create_react_agent, AgentExecutor
agent = create_react_agent(
llm=llm,
prompt=prompt
)
参数解释:llm:我们所定义的tongyi大模型。prompt:通过提示词模板得到的系统提示词。
接下来添加一个代理执行器
agent_executor = AgentExecutor(
agent=agent,
verbose=True, # 建议开发时开启,查看 Agent 决策过程
handle_parsing_errors=True,
max_iterations=15 # 防止无限循环
)
还要修改RunnableWithMessageHistory
CHAIN_WITH_HISTORY = RunnableWithMessageHistory(
runnable=agent_executor,
get_session_history=get_session_history,
input_messages_key="input",
history_messages_key="chat_history",
)
原来的chain直接就不要了, 这里大写是为了方便后面使用LangServe结合fastapi对外提供服务。
9.2 重构知识检索为tool(LangChain工具调用初识)
但是这样的话RAG检索的功能就失效了,为什么呢。因为当前我们试图将 RAG 的上下文 {context} 注入到 ReAct Agent 的 prompt 中,但当前写法 不会自动触发检索,因为 agent_executor 是一个 AgentExecutor,它不理解 RunnablePassthrough.assign(...) 这类 RAG 链的结构。
正确的做法应该是把retriever 作为工具(tool)提供给 Agent,
在 ReAct 架构中,检索(RAG)应该是一个“工具” ,而不是硬编码在 prompt 里的静态上下文。
LangChain 推荐的方式是:
把
retriever包装成一个 tool(例如叫knowledge_search) ,让 Agent 在需要时主动调用它。
这样既符合 ReAct “按需行动” 的原则,又能避免上下文污染或缺失。
好,我们来改造一下,
同样是在当前的py文件(ai_client.py)下,导入所需要的包并新增工具的定义。
from langchain_core.tools import tool
# ========== 工具定义 ==========
@tool
def knowledge_search(query: str) -> str:
"""从内置知识库检索与 query 最相关的内容片段"""
docs = retriever.get_relevant_documents(query)
return "\n\n".join(doc.page_content for doc in docs)
这里的 @tool 是一个 装饰器(decorator) ,它来自 LangChain 框架(具体是 langchain_core.tools 模块),作用是 将一个普通的 Python 函数转换为 LangChain 可识别的“工具(Tool)”对象。
接着还需要定义一个工具的列表tools用来存放我们当前已经定义的知识检索工具,以及后续的一些其他工具,并在agent和AgrntExector中使用工具
代码如下:
tools = [knowledge_search]
# ========== 创建 Agent ==========
agent = create_react_agent(
llm=llm,
tools=tools,
prompt=prompt
)
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True, # 建议开发时开启,查看 Agent 决策过程
handle_parsing_errors=True,
max_iterations=15 # 防止无限循环
)
这时候**format_docs 函数**就没用了,因为我们已经在知识搜索工具中写了相同的逻辑,这时候把他删掉就行了,或者说是我们在知识搜索工具中复用这个函数。
@tool
def knowledge_search(query: str) -> str:
"""从内置知识库检索与 query 最相关的内容片段"""
docs = retriever.get_relevant_documents(query)
return format_docs(docs)
同时为了避免没有知识文档的时候或者说是向量数据库不可用的时候,我们直接返回一个空的检索器retriever,我们编写一个辅助的工具类,并在加载向量数据库出现异常的时候使用他。
class EmptyRetriever(BaseRetriever):
"""当无文档或向量库不可用时,返回空结果的 retriever"""
def _get_relevant_documents(self, query: str) -> List[Document]:
return []
完整的改造之后的代码如下(包含测试用例):
# ai_client.py
import asyncio
import os
from langchain.agents import AgentExecutor, create_react_agent
from langchain.text_splitter import CharacterTextSplitter
from langchain_community.chat_models import ChatTongyi
from langchain_community.document_loaders import DirectoryLoader, TextLoader, PyPDFLoader
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.tools import tool
import config
from message_history.async_pg_history import AsyncPostgresChatMessageHistory
# 注入 DashScope API Key
if config.DASHSCOPE_API_KEY:
os.environ["DASHSCOPE_API_KEY"] = config.DASHSCOPE_API_KEY
# 初始化 LLM
llm = ChatTongyi(
model=config.MODEL,
temperature=config.TEMPERATURE,
)
# ========== 加载文档 ==========
all_docs = []
try:
txt_docs = DirectoryLoader(
config.DOC_DIR,
glob="**/*.txt",
loader_cls=TextLoader,
show_progress=True,
use_multithreading=True,
silent_errors=True
).load()
md_docs = DirectoryLoader(
config.DOC_DIR,
glob="**/*.md",
loader_cls=TextLoader,
show_progress=True,
use_multithreading=True,
silent_errors=True
).load()
pdf_docs = DirectoryLoader(
config.DOC_DIR,
glob="**/*.pdf",
loader_cls=PyPDFLoader,
show_progress=True,
use_multithreading=True,
silent_errors=True
).load()
all_docs = (txt_docs or []) + (md_docs or []) + (pdf_docs or [])
except Exception as e:
print(f"文档加载失败: {e}")
all_docs = []
# 分块
text_splitter = CharacterTextSplitter(separator="\n\n", chunk_size=500, chunk_overlap=80)
texts = text_splitter.split_documents(all_docs) if all_docs else []
# 初始化 Embeddings
embeddings = DashScopeEmbeddings(model="text-embedding-v2")
# 初始化向量数据库(Chroma)
if texts:
vectorstore = Chroma.from_documents(
documents=texts,
embedding=embeddings,
persist_directory=config.EMBEDDINGS_DIR
)
else:
try:
vectorstore = Chroma(
persist_directory=config.EMBEDDINGS_DIR,
embedding_function=embeddings
)
except Exception as e:
print("无文档且无法加载向量库,将使用纯 LLM 模式(无检索)")
vectorstore = None
# 初始化检索器
if vectorstore:
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
else:
retriever = lambda query: []
# 格式化检索结果
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
system_prompt = """你是一名资深 Java 开发 Agent,精通 JDK 8-17、Maven/Gradle、Spring 生态(Spring Boot、Spring MVC、Spring Data JPA、Spring Cloud)、并发与性能调优、JVM 诊断、微服务工程实践。你的目标是在尽可能少的步数内,为用户提供专业、可直接运行的 Java 方案(代码、命令、配置与解释)。
必须遵循:
1. 只使用下方列出的工具;确需外部信息优先用知识库检索,其次再搜索网络。
2. 每次只能使用一个工具;用完等待 Observation 再决定下一步。
3. 回答必须自洽、可执行:给出完整代码需包含必要 import、pom.xml/gradle 配置要给出关键依赖,命令附上执行目录与前置条件。
4. 优先中文回答;代码用合适语言高亮,例如 ```java、```xml、```bash。
5. 安全与最小影响:涉及 shell/文件操作要注明作用与风险,谨慎执行写入/覆盖类操作。
6. 如需求不清,先用 1-3 句澄清关键约束(JDK 版本、构建工具、框架版本、运行环境等)。
Java 解决方案要求:
- 代码质量:命名清晰、边界条件、异常处理、日志与注释(只解释“为什么”)。
- 并发与性能:合理使用线程池、CompletableFuture、锁与无锁结构;避免阻塞;给出复杂度与潜在瓶颈;必要时提供 JMH/压测建议。
- Spring 规范:分层清晰(controller/service/repository)、DTO/VO 转换、事务传播与隔离级别、校验与全局异常处理。
- 数据访问:JPA/Query 方法/Specification/原生 SQL 的取舍;连接池与 N+1 问题规避;分页与索引建议。
- 构建与运行:提供 Maven/Gradle 脚本或命令;测试用例(JUnit5/MockMVC/Mockito)示例;Docker/容器化要点(如需要)。
- JVM 与运维:必要时给出 GC、内存、线程与诊断命令(jcmd/jmap/jstack)、常见参数与可观测性建议。
可用工具:
{tools}
工具使用原则:
- knowledge_search:优先检索本地知识库(如项目内 `doc/` 的最佳实践、调优笔记),再综合回答。
- search_web / crawl_url:仅当知识库不足时使用,并核对来源可靠性。
- read_from_file / write_to_file:仅当用户明确要求“保存/导出/写入/生成文件”时才使用;否则不要调用。写入前在回答里说明将要写入的路径与用途。
- run_shell_command:仅在需要验证构建/测试/脚手架时使用,先说明命令作用与期望输出。
输出格式(ReAct):
格式要求:Action Input 必须是严格 JSON 对象,不要使用代码块或额外文本。
# 输出必须严格遵循以下格式,不要有任何额外内容、解释或 Markdown:
Question: 用户的问题
Thought: 我是否需要使用工具?(是/否)
Action: 工具名称(必须是 [{tool_names}] 之一,若不需要工具则跳过 Action 和 Action Input)
Action Input: {{ "query": "关键词" }}
Observation: 工具返回结果
...(可重复)
Thought: 我现在知道答案了。
Final Answer: 简明回答,包含代码/配置/命令(如需要)
# 注意:
- 如果不需要工具,直接输出 Final Answer,不要写 Action。
- Action Input 必须是合法 JSON,不要用代码块。
- 每个字段独占一行,以字段名开头(如 "Thought:")。
"""
# 构建 Prompt
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{input}"),
("human", "{agent_scratchpad}")
])
# ========== 工具定义 ==========
@tool
def knowledge_search(query: str) -> str:
"""从内置知识库检索与 query 最相关的内容片段"""
docs = retriever.get_relevant_documents(query)
return "\n\n".join(doc.page_content for doc in docs)
TOOLS=[knowledge_search]
agent = create_react_agent(
llm=llm,
tools=TOOLS,
prompt=prompt
)
agent_executor = AgentExecutor(
agent=agent,
tools=TOOLS,
verbose=True, # 建议开发时开启,查看 Agent 决策过程
handle_parsing_errors=True,
max_iterations=15 # 防止无限循环
)
# 获取对话历史,
def get_session_history(session_id: str):
return AsyncPostgresChatMessageHistory(session_id=session_id)
# ========== 带记忆的完整链 ==========
CHAIN_WITH_HISTORY = RunnableWithMessageHistory(
runnable=agent_executor,
get_session_history=get_session_history,
input_messages_key="input",
history_messages_key="chat_history",
)
async def test_java_threadpool_multiturn():
session_id = "java_pool_test_001"
print("开始 Java 线程池多轮会话测试(基于知识库)\n")
# 第1轮:泛问线程池最佳实践
print("第1轮:问「Java 线程池最佳实践」")
resp1 = await CHAIN_WITH_HISTORY.ainvoke(
{"input": "Java 线程池的最佳实践有哪些?(之后所有的回答尽你最大的可能精简,以减少输出)"},
config={"configurable": {"session_id": session_id}}
)
print("回答:\n", resp1["output"].strip()[:300] + "...\n")
# 第2轮:追问 IO 密集型配置
print("第2轮:追问「IO 密集型任务该怎么配置线程池?」")
resp2 = await CHAIN_WITH_HISTORY.ainvoke(
{"input": "IO 密集型任务该怎么配置线程池?"},
config={"configurable": {"session_id": session_id}}
)
print("回答:\n", resp2["output"].strip()[:200] + "...\n")
# 第3轮:问拒绝策略
print("第3轮:问「推荐用什么拒绝策略?为什么?」")
resp3 = await CHAIN_WITH_HISTORY.ainvoke(
{"input": "推荐用什么拒绝策略?为什么?"},
config={"configurable": {"session_id": session_id}}
)
print("回答:\n", resp3["output"].strip()[:200] + "...\n")
# 第4轮:要求给出完整代码示例
print("第4轮:要求「给出一个生产可用的线程池构造代码」")
resp4 = await CHAIN_WITH_HISTORY.ainvoke(
{"input": "请给出一个生产可用的线程池构造代码示例"},
config={"configurable": {"session_id": session_id}}
)
print("回答:\n", resp4["output"].strip()[:300] + "...\n")
# 第5轮:陷阱问题(测试是否编造)
print("第5轮:问「文档里提到用 ForkJoinPool 吗?」")
resp5 = await CHAIN_WITH_HISTORY.ainvoke(
{"input": "你刚才的知识库里提到要用 ForkJoinPool 吗?"},
config={"configurable": {"session_id": session_id}}
)
print("回答:\n", resp5["output"].strip()[:150] + "...\n")
print("多轮测试完成!")
if __name__ == "__main__":
asyncio.run(test_java_threadpool_multiturn())
我们来运行一下:
开始 Java 线程池多轮会话测试(基于知识库)
第1轮:问「Java 线程池最佳实践」
> Entering new AgentExecutor chain...
Invalid Format: Missing 'Action:' after 'Thought:'Thought: 无需使用工具,直接回答问题。
Final Answer:
1. 显式创建 `ThreadPoolExecutor`。
2. 核心线程数:CPU 密集型 `N+1`,IO 密集型 `2N`(N=CPU核数)。
3. 有限队列容量。
4. 自定义线程命名。
5. 拒绝策略用 `CallerRunsPolicy`。
6. 启用 `allowCoreThreadTimeOut(true)`。
7. 监控关键指标。
> Finished chain.
回答:
1. 显式创建 `ThreadPoolExecutor`。
2. 核心线程数:CPU 密集型 `N+1`,IO 密集型 `2N`(N=CPU核数)。
3. 有限队列容量。
4. 自定义线程命名。
5. 拒绝策略用 `CallerRunsPolicy`。
6. 启用 `allowCoreThreadTimeOut(true)`。
7. 监控关键指标。...
第2轮:追问「IO 密集型任务该怎么配置线程池?」
> Entering new AgentExecutor chain...
Invalid Format: Missing 'Action:' after 'Thought:'Thought: 我现在知道答案了。
Final Answer: 你的配置已经很合理,适用于生产环境的IO密集型任务。以下是精简后的代码示例:
```java
int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
int maxPoolSize = corePoolSize * 2;
int queueCapacity = 1000;
long keepAliveSeconds = 60L;
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveSeconds,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(queueCapacity),
r -> {
Thread t = new Thread(r, "IO-pool-" + Thread.currentThread().getId());
t.setDaemon(false);
return t;
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
executor.allowCoreThreadTimeOut(true);
```
- 核心线程数:2×CPU
- 最大线程数:4×CPU
- 队列容量:1000
- 保持时间:60秒
- 自定义线程命名
- 拒绝策略:`CallerRunsPolicy`
- 启用 `allowCoreThreadTimeOut`
> Finished chain.
回答:
你的配置已经很合理,适用于生产环境的IO密集型任务。以下是精简后的代码示例:
```java
int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
int maxPoolSize = corePoolSize * 2;
int queueCapacity = 1000;
long keepAliveSeconds =...
9.3 ReAct输出解析器报错(优化提示词)
会发现他总是有一个Invalid Format: Missing 'Action:' after 'Thought:',这是因为qwen模型没有按照ReAct的模式严格输出,而LangChain的ReAct解析器是要求必须按照这种格式输出的,尽管我们在提示词中严格要求了,但是还是没有用。这就是lllm的问提了,
我们想实现的功能确实是实现了,而且PostgreSQL有对话记录
9.4 工具开发
接下来我们来给llm开发其他可供调用的工具
首先在我们的项目的根目录新建tools文件夹,并新建ai_tools.py文件
# ai_tools.py
# 精简工具库(百度搜索 / 文件 / PDF / 命令 / HTTP)| 2025-09
import os
import subprocess
import sys
import trafilatura
import requests
from pydantic import BaseModel, Field, model_validator
from datetime import datetime
from urllib.parse import quote
from typing import Dict, List, Optional
import re
from io import BytesIO, StringIO
from bs4 import BeautifulSoup
from langchain_core.tools import tool
# ================
# 🖼️ PDF 支持中文
# ================
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer
try:
pdfmetrics.registerFont(TTFont('SimHei', 'simhei.ttf'))
except Exception:
pass
# ================
# 🔐 安全配置
# ================
ALLOWED_COMMANDS = ["ls", "cat", "echo", "pwd", "python", "pip", "curl", "wget", "date", "uname"]
DANGEROUS_PATTERNS = [
r"rm\s+-rf\s+/",
r"\bdd\b",
r"\bshutdown\b",
r"\breboot\b",
r":$${\s*:|\s*:&\s*};:", # fork bomb
r"wget.*-O\s+/etc/",
r"curl.*-o\s+/etc/",
]
# ================
# 🛠️ 工具参数 Schema 定义
# ================
class IsSafeCommandInput(BaseModel):
command: str = Field(..., description="要检查的 Shell 命令,例如 'rm -rf /'")
class RunShellCommandInput(BaseModel):
command: str = Field(..., description="要执行的 Shell 命令")
timeout: int = Field(30, description="命令执行超时时间(秒),默认 30")
class ExecutePythonCodeInput(BaseModel):
code: str = Field(..., description="要执行的 Python 代码片段")
class WriteToFileInput(BaseModel):
filename: str = Field(..., description="要写入的文件路径,例如 'docs/report.txt'")
content: Optional[str] = Field(None, description="要写入的文本内容,缺省则写入空文件")
@model_validator(mode="before")
@classmethod
def normalize_payload(cls, value):
# 兼容多种异常输入:
# 1) 只有 filename,其值是字符串化的 JSON:'{"filename":"a.txt","content":"..."}'
# 2) 仅传别名字段:file/text
# 3) 直接传入 JSON 字符串
try:
if isinstance(value, dict):
# 合并 filename 内嵌 JSON
fn = value.get("filename")
if isinstance(fn, str):
s = fn.strip()
if s.startswith("{") and s.endswith("}"):
import json as _json
try:
inner = _json.loads(s)
merged = {**value, **inner}
value = merged
except Exception:
pass
# 别名兼容
if "file" in value and "filename" not in value:
value["filename"] = value["file"]
if "text" in value and "content" not in value:
value["content"] = value["text"]
return value
if isinstance(value, str):
s = value.strip()
if s.startswith("{") and s.endswith("}"):
import json as _json
try:
return _json.loads(s)
except Exception:
return value
return value
except Exception:
return value
class ReadFromFileInput(BaseModel):
filename: str = Field(..., description="要读取的文件路径")
class CreatePdfInput(BaseModel):
content: str = Field(..., description="PDF 正文内容,支持换行")
title: str = Field("文档", description="PDF 文档标题")
filename: str = Field("output.pdf", description="生成的 PDF 文件名,例如 'report.pdf'")
class SearchWebInput(BaseModel):
query: str = Field(..., description="要在百度上搜索的关键词")
num_results: int = Field(5, description="返回结果数量,默认 5 条")
class CallApiInput(BaseModel):
url: str = Field(..., description="要调用的 API 地址")
method: str = Field("GET", description="HTTP 方法,如 GET、POST、PUT、DELETE")
data: Optional[Dict] = Field(None, description="POST/PUT 请求的 JSON 数据")
headers: Optional[Dict] = Field(None, description="自定义请求头")
class CrawlUrlInput(BaseModel):
url: str = Field(..., description="要抓取的网页 URL")
max_bytes: int = Field(200_000, description="最大抓取内容大小(字节),默认 200KB")
# ================
# 🛠️ 常用工具(带 args_schema)
# ================
@tool(args_schema=IsSafeCommandInput)
def is_safe_command(command: str) -> bool:
"""检查命令是否安全"""
cmd = command.strip().lower()
if any(re.search(pattern, cmd) for pattern in DANGEROUS_PATTERNS):
return False
cmd_base = cmd.split()[0]
if cmd_base not in ALLOWED_COMMANDS:
return False
return True
@tool
def get_current_time() -> str:
"""获取当前时间"""
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
@tool
def get_weather(city: str = "北京") -> str:
"""模拟获取天气(可替换为真实 API)"""
return f"模拟天气:{city} 当前晴,25°C,湿度 60%"
@tool(args_schema=RunShellCommandInput)
def run_shell_command(command: str, timeout: int = 30) -> Dict:
"""执行 Shell 命令(带安全检查)"""
if not is_safe_command(command):
return {"error": "命令不安全或不在白名单中", "command": command}
try:
result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=timeout)
return {
"returncode": result.returncode,
"stdout": result.stdout,
"stderr": result.stderr,
"command": command
}
except subprocess.TimeoutExpired:
return {"error": "命令执行超时", "command": command}
except Exception as e:
return {"error": str(e), "command": command}
@tool(args_schema=ExecutePythonCodeInput)
def execute_python_code(code: str) -> Dict:
"""执行 Python 代码(简单沙箱)"""
try:
old_stdout = sys.stdout
sys.stdout = captured_output = StringIO()
exec(code, {})
sys.stdout = old_stdout
return {"success": True, "output": captured_output.getvalue()}
except Exception as e:
return {"success": False, "error": str(e)}
@tool(args_schema=WriteToFileInput)
def write_to_file(filename: str, content: Optional[str] = None) -> Dict:
"""写入文件"""
try:
# 兼容把整段 JSON 放进 filename 的异常情况
if isinstance(filename, str):
s = filename.strip()
if s.startswith('{') and s.endswith('}'):
import json as _json
try:
obj = _json.loads(s)
filename = obj.get('filename', 'output.txt')
content = obj.get('content', content)
except Exception:
pass
# 清洗与截断文件名,避免 ENAMETOOLONG
filename = re.sub(r'[<>:"/\|?*\x00-\x1f]', '_', filename or 'output.txt')
base_dir = os.path.dirname(filename)
name_only = os.path.basename(filename)
if len(name_only) > 120:
root, ext = os.path.splitext(name_only)
name_only = (root[:100] + '_') + ext[:10]
filename = os.path.join(base_dir, name_only) if base_dir else name_only
os.makedirs(os.path.dirname(filename) if os.path.dirname(filename) else '.', exist_ok=True)
with open(filename, 'w', encoding='utf-8') as f:
f.write(content or "")
return {"success": True, "path": os.path.abspath(filename)}
except Exception as e:
return {"error": str(e)}
@tool(args_schema=ReadFromFileInput)
def read_from_file(filename: str) -> Dict:
"""读取文件"""
try:
with open(filename, 'r', encoding='utf-8') as f:
content = f.read()
return {"content": content}
except Exception as e:
return {"error": str(e)}
@tool(args_schema=CreatePdfInput)
def create_pdf(content: str, title: str = "文档", filename: str = "output.pdf") -> Dict:
"""创建支持中文的 PDF"""
try:
filename = re.sub(r'[<>:"/\|?*\x00-\x1f]', '_', filename)
os.makedirs(os.path.dirname(filename) if os.path.dirname(filename) else '.', exist_ok=True)
buffer = BytesIO()
doc = SimpleDocTemplate(buffer, pagesize=A4)
styles = getSampleStyleSheet()
chinese_style = ParagraphStyle(
'chinese', parent=styles['Normal'],
fontName='SimHei', fontSize=12, leading=16, spaceAfter=12
)
story = [Paragraph(f"<b>{title}</b>", chinese_style), Spacer(1, 12)]
for para in content.split('\n'):
if para.strip():
story.append(Paragraph(para.strip(), chinese_style))
story.append(Spacer(1, 6))
doc.build(story)
with open(filename, 'wb') as f:
f.write(buffer.getvalue())
return {
"success": True,
"path": os.path.abspath(filename),
"size": os.path.getsize(filename)
}
except Exception as e:
return {"error": f"PDF 生成失败: {str(e)}"}
@tool(args_schema=SearchWebInput)
def search_web(query: str, num_results: int = 5) -> List[Dict]:
"""使用百度搜索,返回标题/链接/摘要"""
try:
url = f"https://www.baidu.com/s?wd={quote(query)}"
headers = {"User-Agent": "Mozilla/5.0"}
resp = requests.get(url, headers=headers, timeout=10)
resp.raise_for_status()
soup = BeautifulSoup(resp.text, 'lxml')
items = []
for h3 in soup.select('h3.t, h3.c-title'):
a = h3.find('a')
if not a:
continue
title = a.get_text(strip=True)
href = a.get('href')
snippet_tag = h3.find_next_sibling('div')
snippet = snippet_tag.get_text(strip=True) if snippet_tag else ''
items.append({"title": title, "url": href, "snippet": snippet})
if len(items) >= num_results:
break
return items or [{"url": url}]
except Exception as e:
return [{"error": str(e)}]
@tool(args_schema=CallApiInput)
def call_api(url: str, method: str = "GET", data: Dict = None, headers: Dict = None) -> Dict:
"""调用任意 HTTP API"""
try:
response = requests.request(
method=method.upper(),
url=url,
json=data,
headers=headers or {},
timeout=15
)
return {
"status_code": response.status_code,
"json": response.json() if 'application/json' in response.headers.get('content-type', '') else None,
"text": response.text
}
except Exception as e:
return {"error": str(e)}
@tool(args_schema=CrawlUrlInput)
def crawl_url(url: str, max_bytes: int = 200_000) -> dict:
"""
抓取网页并提取核心正文内容(去广告、去导航、保留主要文本)。
使用 trafilatura 进行智能内容提取。
"""
try:
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
}
resp = requests.get(url, headers=headers, timeout=15)
resp.raise_for_status()
html_content = resp.text
if len(html_content) > max_bytes:
html_content = html_content[:max_bytes]
downloaded = trafilatura.extract(
html_content,
no_fallback=False,
include_comments=False,
include_tables=True,
include_formatting=False,
output_format='txt',
)
if not downloaded:
return {"success": False, "error": "无法提取核心内容,可能是动态页面或结构复杂"}
metadata = trafilatura.metadata.extract_metadata(html_content)
title = (metadata.title if metadata and metadata.title
else resp.url.split("/")[-1].split("?")[0] or "未命名页面")
return {
"success": True,
"url": url,
"title": title,
"text": downloaded.strip()
}
except Exception as e:
return {"success": False, "error": str(e)}
这些工具的代码大家自己ai生成也行(我就是ai生成的).
我们刚刚不是定义了知识搜索工具么,现在再来导入其他的工具,编写代码如下:
在刚才我们定义的TOOLS下面接着写
from tools.ai_tools import (
get_current_time,
get_weather,
run_shell_command,
execute_python_code,
write_to_file,
read_from_file,
create_pdf,
call_api,
search_web,
crawl_url,
)
TOOLS = [
knowledge_search,
search_web,
crawl_url,
get_current_time,
get_weather,
read_from_file,
write_to_file,
create_pdf,
call_api,
run_shell_command,
execute_python_code,
]
从刚才的工具py文件中导入。并优化系统提示词如下,注意:这并不是我们随意写的系统提示词,而是按照LangChain严格的要求来的,
9.5 系统提示词优化
其中{tools}以及Tought Action Observation等都是官方严格要求必须有的,因为ReAct模式使用的是LangChain的ReAct输出解析器,除非我们重写这个输出解析器的逻辑.
system_prompt = """你是一名资深 Java 开发 Agent,精通 JDK 8-17、Maven/Gradle、Spring 生态(Spring Boot、Spring MVC、Spring Data JPA、Spring Cloud)、并发与性能调优、JVM 诊断、微服务工程实践。你的目标是在尽可能少的步数内,为用户提供专业、可直接运行的 Java 方案(代码、命令、配置与解释)。
必须遵循:
1. 只使用下方列出的工具;确需外部信息优先用知识库检索,其次再搜索网络。
2. 每次只能使用一个工具;用完等待 Observation 再决定下一步。
3. 回答必须自洽、可执行:给出完整代码需包含必要 import、pom.xml/gradle 配置要给出关键依赖,命令附上执行目录与前置条件。
4. 优先中文回答;代码用合适语言高亮,例如 ```java、```xml、```bash。
5. 安全与最小影响:涉及 shell/文件操作要注明作用与风险,谨慎执行写入/覆盖类操作。
6. 如需求不清,先用 1-3 句澄清关键约束(JDK 版本、构建工具、框架版本、运行环境等)。
Java 解决方案要求:
- 代码质量:命名清晰、边界条件、异常处理、日志与注释(只解释“为什么”)。
- 并发与性能:合理使用线程池、CompletableFuture、锁与无锁结构;避免阻塞;给出复杂度与潜在瓶颈;必要时提供 JMH/压测建议。
- Spring 规范:分层清晰(controller/service/repository)、DTO/VO 转换、事务传播与隔离级别、校验与全局异常处理。
- 数据访问:JPA/Query 方法/Specification/原生 SQL 的取舍;连接池与 N+1 问题规避;分页与索引建议。
- 构建与运行:提供 Maven/Gradle 脚本或命令;测试用例(JUnit5/MockMVC/Mockito)示例;Docker/容器化要点(如需要)。
- JVM 与运维:必要时给出 GC、内存、线程与诊断命令(jcmd/jmap/jstack)、常见参数与可观测性建议。
可用工具:
{tools}
工具使用原则:
- knowledge_search:优先检索本地知识库(如项目内 `doc/` 的最佳实践、调优笔记),再综合回答。
- search_web / crawl_url:仅当知识库不足时使用,并核对来源可靠性。
- read_from_file / write_to_file:仅当用户明确要求“保存/导出/写入/生成文件”时才使用;否则不要调用。写入前在回答里说明将要写入的路径与用途。
- run_shell_command:仅在需要验证构建/测试/脚手架时使用,先说明命令作用与期望输出。
输出格式(ReAct):
格式要求:Action Input 必须是严格 JSON 对象,不要使用代码块或额外文本。
使用格式:
Question: 用户的问题
Thought: 你应该做什么?是否需要使用工具?
如果不需要使用工具(例如:回答简单问候、解释概念、提供帮助等),直接输出:
Thought: 我现在知道最终答案
Answer: (直接给出答案,不要任何Action/Action Input)
如果需要使用工具,按以下格式:
Thought: (说明为什么需要使用工具)
Action: 工具名称,必须是 [{tool_names}] 中的一个
Action Input: 工具输入参数(JSON格式)
Observation: 工具返回结果
... (此循环可以重复多次直到获得足够信息)
Thought: 我现在知道最终答案
Answer:
- 简要结论(1-3 句)
- 关键步骤/命令
- 完整代码/配置(如有)
- 验证方法与可能的风险/注意事项
重要规则:
- 对于简单问候("你好"、"hi"、"hello"等),直接回答,不要使用任何工具
- 对于概念解释、问题澄清、提供帮助等场景,直接回答,不要使用任何工具
- 只有在需要获取外部信息、执行命令、读写文件等情况下才使用工具
"""
至此,我们的智能体也有了其他的工具可以使用。
接下来,我们先不做一个测试,我们在实现另外一个功能。想一想,我们目前实现的智能体的性能怎么样或者说是相应的速度怎么样(这个当然是llm本身决定的)。那有没有什么优化的方式呢。试想一下,如果有很多人在使用我们的智能体,就说在Java开发中吧,有没有可能,很多程序员都遇到了相似的问题,比如:遇到spring中的循环依赖问题,该怎么解决,是不是解决方式就哪几种啊。那我们的agent还有必要每次都重新生成解决方案吗,是不是没有必要,那就很明显了,是不是我们把问题和回答缓存起来就好了,到时候有相似的问题直接读取缓存,这样不仅节约了token也提高了我们agent的性能。我们就来实现一下。
9.6 性能优化(基于Redis的语义化缓存)
在这里我们使用Redis作为缓存,并实现语义化的匹配。什么是语义化匹配呢?举一个例子,在Redis有一条:“怎么解决循环依赖问题?”
用户的问题是:怎么处理循环依赖的报错?,这两个问题是不是都是同一个意思,好了,这就是语义化,根据两个问题的意思,去匹配是否是同一个问题而不是直接根据字符去匹配。
注意这里实现语义化需要进行文本嵌入模型进行向量的转化,所以普通的Redis是不能用的,需要支持向量数据的Redis。一下是docker一键安装命令
docker run -d \
--name redis-stack \
-p 6379:6379 \
-p 8001:8001 \
redis/redis-stack:latest
那么就来实现一下,首先需要安装Redis库(使用pip安装或者其他方式也可以),假设你已经安装好了。
编写如下代码在ai_agent.py中
from langchain.globals import set_llm_cache
try:
if config.REDIS_URL:
redis_cache = RedisSemanticCache(redis_url=config.REDIS_URL, embedding=embeddings)
set_llm_cache(redis_cache)
print("Redis 语义缓存已启用")
except Exception as e:
print(f"Redis 缓存初始化失败: {e}")
注意,config中要自己写好Redis的配置。
完整ai_agent.py代码如下:
from langchain.globals import set_llm_cache
from langchain.text_splitter import CharacterTextSplitter
from langchain_community.cache import RedisSemanticCache
from langchain_community.chat_models import ChatTongyi
from langchain_community.document_loaders import DirectoryLoader, TextLoader, PyPDFLoader
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder, SystemMessagePromptTemplate
from langchain_core.runnables import RunnableWithMessageHistory
from langchain_core.tools import tool
from langchain.agents import create_react_agent, AgentExecutor
import os
from operator import itemgetter
import config
from message_history import sql_message_history
# 注入 DashScope API Key
if config.DASHSCOPE_API_KEY:
os.environ["DASHSCOPE_API_KEY"] = config.DASHSCOPE_API_KEY
# ========== LLM ==========
llm = ChatTongyi(
model=config.MODEL,
temperature=config.TEMPERATURE,
)
# ========== 知识库:加载 TXT / MD / PDF ==========
all_docs = []
try:
txt_docs = DirectoryLoader(
config.DOC_DIR,
glob="**/*.txt",
loader_cls=TextLoader,
show_progress=True,
use_multithreading=True,
silent_errors=True
).load()
md_docs = DirectoryLoader(
config.DOC_DIR,
glob="**/*.md",
loader_cls=TextLoader,
show_progress=True,
use_multithreading=True,
silent_errors=True
).load()
pdf_docs = DirectoryLoader(
config.DOC_DIR,
glob="**/*.pdf",
loader_cls=PyPDFLoader,
show_progress=True,
use_multithreading=True,
silent_errors=True
).load()
all_docs = (txt_docs or []) + (md_docs or []) + (pdf_docs or [])
except Exception as e:
print(f"文档加载失败: {e}")
all_docs = []
text_splitter = CharacterTextSplitter(separator="\n\n", chunk_size=500, chunk_overlap=80, length_function=len)
texts = text_splitter.split_documents(all_docs) if all_docs else []
embeddings = DashScopeEmbeddings(model="text-embedding-v2")
vectorstore = None
if os.path.exists(config.EMBEDDINGS_DIR):
try:
vectorstore = Chroma(
embedding_function=embeddings,
persist_directory=config.EMBEDDINGS_DIR,
collection_name=config.COLLECTION_NAME,
)
except Exception as e:
print(f"加载向量数据库失败: {e}")
vectorstore = None
if vectorstore is None or not texts:
vectorstore = Chroma.from_documents(
documents=texts,
embedding=embeddings,
persist_directory=config.EMBEDDINGS_DIR,
collection_name=config.COLLECTION_NAME,
)
else:
try:
if texts:
new_ids = vectorstore.add_documents(texts)
print(f"新增 {len(new_ids)} 个文档片段")
except Exception as e:
print(f"增量更新向量库失败: {e}")
retriever = vectorstore.as_retriever(search_kwargs={"k": config.RAG_TOP_K})
# ========== 工具定义 ==========
@tool
def knowledge_search(query: str) -> str:
"""从内置知识库检索与 query 最相关的内容片段"""
docs = retriever.get_relevant_documents(query)
return "\n\n".join(doc.page_content for doc in docs)
# 导入外部工具
from tools.ai_tools import (
get_current_time,
get_weather,
run_shell_command,
execute_python_code,
write_to_file,
read_from_file,
create_pdf,
call_api,
search_web,
crawl_url,
)
TOOLS = [
knowledge_search,
search_web,
crawl_url,
get_current_time,
get_weather,
read_from_file,
write_to_file,
create_pdf,
call_api,
run_shell_command,
execute_python_code,
]
system_prompt = """你是一名资深 Java 开发 Agent,精通 JDK 8-17、Maven/Gradle、Spring 生态(Spring Boot、Spring MVC、Spring Data JPA、Spring Cloud)、并发与性能调优、JVM 诊断、微服务工程实践。你的目标是在尽可能少的步数内,为用户提供专业、可直接运行的 Java 方案(代码、命令、配置与解释)。
必须遵循:
1. 只使用下方列出的工具;确需外部信息优先用知识库检索,其次再搜索网络。
2. 每次只能使用一个工具;用完等待 Observation 再决定下一步。
3. 回答必须自洽、可执行:给出完整代码需包含必要 import、pom.xml/gradle 配置要给出关键依赖,命令附上执行目录与前置条件。
4. 优先中文回答;代码用合适语言高亮,例如 ```java、```xml、```bash。
5. 安全与最小影响:涉及 shell/文件操作要注明作用与风险,谨慎执行写入/覆盖类操作。
6. 如需求不清,先用 1-3 句澄清关键约束(JDK 版本、构建工具、框架版本、运行环境等)。
Java 解决方案要求:
- 代码质量:命名清晰、边界条件、异常处理、日志与注释(只解释“为什么”)。
- 并发与性能:合理使用线程池、CompletableFuture、锁与无锁结构;避免阻塞;给出复杂度与潜在瓶颈;必要时提供 JMH/压测建议。
- Spring 规范:分层清晰(controller/service/repository)、DTO/VO 转换、事务传播与隔离级别、校验与全局异常处理。
- 数据访问:JPA/Query 方法/Specification/原生 SQL 的取舍;连接池与 N+1 问题规避;分页与索引建议。
- 构建与运行:提供 Maven/Gradle 脚本或命令;测试用例(JUnit5/MockMVC/Mockito)示例;Docker/容器化要点(如需要)。
- JVM 与运维:必要时给出 GC、内存、线程与诊断命令(jcmd/jmap/jstack)、常见参数与可观测性建议。
可用工具:
{tools}
工具使用原则:
- knowledge_search:优先检索本地知识库(如项目内 `doc/` 的最佳实践、调优笔记),再综合回答。
- search_web / crawl_url:仅当知识库不足时使用,并核对来源可靠性。
- read_from_file / write_to_file:仅当用户明确要求“保存/导出/写入/生成文件”时才使用;否则不要调用。写入前在回答里说明将要写入的路径与用途。
- run_shell_command:仅在需要验证构建/测试/脚手架时使用,先说明命令作用与期望输出。
输出格式(ReAct):
格式要求:Action Input 必须是严格 JSON 对象,不要使用代码块或额外文本。
使用格式:
Question: 用户的问题
Thought: 你应该做什么?是否需要使用工具?
如果不需要使用工具(例如:回答简单问候、解释概念、提供帮助等),直接输出:
Thought: 我现在知道最终答案
Answer: (直接给出答案,不要任何Action/Action Input)
如果需要使用工具,按以下格式:
Thought: (说明为什么需要使用工具)
Action: 工具名称,必须是 [{tool_names}] 中的一个
Action Input: 工具输入参数(JSON格式)
Observation: 工具返回结果
... (此循环可以重复多次直到获得足够信息)
Thought: 我现在知道最终答案
Answer:
- 简要结论(1-3 句)
- 关键步骤/命令
- 完整代码/配置(如有)
- 验证方法与可能的风险/注意事项
重要规则:
- 对于简单问候("你好"、"hi"、"hello"等),直接回答,不要使用任何工具
- 对于概念解释、问题澄清、提供帮助等场景,直接回答,不要使用任何工具
- 只有在需要获取外部信息、执行命令、读写文件等情况下才使用工具
"""
prompt = ChatPromptTemplate.from_messages([
SystemMessagePromptTemplate.from_template(system_prompt),
MessagesPlaceholder(variable_name="chat_history"), # 支持历史消息
("human", "{input}\n\n{agent_scratchpad}"),
])
# ========== 创建 Agent ==========
agent = create_react_agent(
llm=llm,
tools=TOOLS,
prompt=prompt
)
agent_executor = AgentExecutor(
agent=agent,
tools=TOOLS,
verbose=True, # 建议开发时开启,查看 Agent 决策过程
handle_parsing_errors=True,
max_iterations=15 # 防止无限循环
)
# 语义缓存
try:
if config.REDIS_URL:
redis_cache = RedisSemanticCache(redis_url=config.REDIS_URL, embedding=embeddings)
set_llm_cache(redis_cache)
print("Redis 语义缓存已启用")
except Exception as e:
print(f"Redis 缓存初始化失败: {e}")
# 会话记忆(SQLChatMessageHistory)
def _get_history(session_id: str):
return sql_message_history.get_session_history(session_id)
CHAIN_WITH_HISTORY = RunnableWithMessageHistory(
runnable=agent_executor,
get_session_history=_get_history,
input_messages_key="input",
history_messages_key="chat_history",
)
print("Agent 初始化完成")
我们先不急着去测试,再来想一个问题,如果别人要使用我们的agent怎么办。相信首先想到的方式就是暴露HTTP接口,将agent服务化。