基于 LangChain + 通义千问打造ReAct私募基金智能问答助手

前言

在金融合规领域,私募基金的运作指引条款繁杂、更新频繁。传统的"关键词匹配"或简单的 RAG(检索增强生成)往往难以处理需要多步推理的复杂问题。

今天,我们将带大家实战构建一个基于 ReAct (Reasoning and Acting) 模式的智能体(Agent)。它不仅能查库,还能像人类一样"思考-行动-观察",最终给出精准答案。

我们将使用 PythonLangChain 和阿里 通义千问 (Qwen-Max) 大模型来实现,并重点讲解如何通过自定义 Prompt 模板增强型输出解析器来解决 Agent 常见的"幻觉"和"格式错误"问题。


1. 系统架构设计

本系统的核心在于 Agent(智能体)。它充当"大脑",接收用户问题,决定调用哪个工具(搜索、问答等),并根据工具返回的结果(Observation)决定下一步行动,直到得出最终答案(Final Answer)。

核心流程图


2. 核心代码实现步骤

步骤一:环境与数据源模拟

首先,我们需要模拟一个私募基金的规则库。在实际生产中,这通常是向量数据库(Vector DB)或 SQL 数据库。

python 复制代码
# 模拟数据
FUND_RULES_DB = [
    {
        "id": "rule001",
        "category": "设立与募集",
        "question": "私募基金的合格投资者标准是什么?",
        "answer": "合格投资者是指具备相应风险识别能力...净资产不低于1000万元的单位..."
    },
    # ... 更多规则
]

# 数据源类:提供检索方法
class FundRulesDataSource:
    def __init__(self, llm):
        self.llm = llm
        self.rules_db = FUND_RULES_DB

    def search_rules_by_keywords(self, keywords: str) -> str:
        """工具实现:关键词搜索"""
        # (省略具体匹配逻辑,详见完整代码)
        return "..." 

    def search_rules_by_category(self, category: str) -> str:
        """工具实现:按类别查询"""
        return "..."

步骤二:定义工具 (Tools)

Agent 需要知道有哪些"手"可以使用。我们将上述方法封装为 LangChain 的 Tool 对象。

python 复制代码
from langchain_core.tools import Tool

def create_tools(data_source):
    return [
        Tool(
            name="关键词搜索",
            func=data_source.search_rules_by_keywords,
            description="通过关键词搜索规则。输入例如:'合格投资者', '募集规模'",
        ),
        Tool(
            name="类别查询",
            func=data_source.search_rules_by_category,
            description="查询特定类别。输入必须是:'设立与募集' 或 '监管规定'",
        ),
        # ... 可以添加更多工具
    ]

步骤三:编写 Prompt 模板(核心优化点)

这是 ReAct 模式的灵魂。我们需要告诉大模型如何"思考"。

优化重点

  1. 明确的 ReAct 格式 :强制要求模型按照 Thought -> Action -> Action Input -> Observation 的顺序输出。
  2. Scratchpad 处理 :在 CustomPromptTemplate 中,我们显式地处理了中间步骤(intermediate_steps),并在 Observation 后追加 \nThought:强制引导模型进入下一轮思考,防止模型不知道接下来说什么。
python 复制代码
AGENT_TMPL = """你是一个专业的私募基金问答助手。你的任务是利用给定工具回答用户问题。

你可以使用以下工具:

{tools}

请严格遵循以下思考流程(Thought/Action/Observation/Final Answer):

1. **Thought**: 思考用户的问题需要什么信息,我应该采取什么行动?
2. **Action**: 选择一个合适的工具,工具名称必须是 [{tool_names}] 之一。
3. **Action Input**: 输入工具所需的参数。
4. **Observation**: 工具返回的结果(这一步由系统自动完成)。
5. **Thought**: 观察结果。如果结果足以回答问题,请直接给出最终答案。
6. **Final Answer**: 给用户的最终回答。

**重要提示**:
- 当你得到 "Observation" 后,请立即思考并给出 "Final Answer"。
- 不要编造工具名称。
- 如果知识库没有相关信息,请在 Final Answer 中诚实说明。

--- 示例 ---
Question: 私募基金的合格投资者标准是什么?
Thought: 我需要查找关于合格投资者的定义。
Action: 关键词搜索
Action Input: 合格投资者标准
Observation: 合格投资者是指...(工具返回内容)
Thought: 我已经获得了关于合格投资者的信息,可以回答用户了。
Final Answer: 私募基金的合格投资者标准是...
--- 示例结束 ---

开始回答:

Question: {input}
{agent_scratchpad}"""

class CustomPromptTemplate(StringPromptTemplate):
    def format(self, **kwargs) -> str:
        intermediate_steps = kwargs.pop("intermediate_steps")
        thoughts = ""
        for action, observation in intermediate_steps:
            thoughts += action.log
            # 【关键技巧】手动拼接 Observation 和 下一步的 Thought 引导词
            thoughts += f"\nObservation: {observation}\nThought: "
        
        kwargs["agent_scratchpad"] = thoughts
        kwargs["tools"] = "\n".join([f"{tool.name}: {tool.description}" for tool in self.tools])
        kwargs["tool_names"] = ", ".join([tool.name for tool in self.tools])
        return self.template.format(**kwargs)

步骤四:输出解析器与容错机制(关键修复)

大模型通过 API 返回的是纯文本,我们需要用正则解析出 ActionAction Input

优化重点

模型有时会"不听话",比如跳过 Action 直接给答案,或者格式微调导致正则失败。我们在 parse 方法中增加了 Fallback(兜底)机制 ,如果正则匹配失败但看起来像是在回答问题,就将其视为 Final Answer,极大提高了系统的鲁棒性

python 复制代码
import re
from langchain_core.agents import AgentAction, AgentFinish
from langchain_classic.agents import AgentOutputParser

class CustomOutputParser(AgentOutputParser):
    def parse(self, llm_output: str):
        llm_output = llm_output.strip()
        
        # 1. 如果包含 Final Answer,直接结束
        if "Final Answer:" in llm_output:
            return AgentFinish(
                return_values={"output": llm_output.split("Final Answer:")[-1].strip()},
                log=llm_output,
            )

        # 2. 尝试正则解析 Action
        regex = r"Action\s*\d*\s*:(.*?)\nAction\s*\d*\s*Input\s*\d*\s*:[\s]*(.*)"
        match = re.search(regex, llm_output, re.DOTALL)
        if match:
            return AgentAction(tool=match.group(1).strip(), tool_input=match.group(2).strip(' "'), log=llm_output)

        # 3. [容错] 如果没找到 Action,假设模型是在直接回答
        # 避免因格式微小错误导致整个程序 Crash
        if not match and "Action:" not in llm_output:
            return AgentFinish(
                return_values={"output": llm_output},
                log=f"Fallback parsing: {llm_output}"
            )
        
        raise ValueError(f"无法解析 LLM 输出: `{llm_output}`")

步骤五:组装 Agent 并运行

最后,我们将 LLM、Prompt、Parser 组装起来。

注意 :设置 stop=["\nObservation:"] 至关重要,这能防止 LLM 产生幻觉(自己编造工具的返回结果)。

python 复制代码
from langchain_classic.agents import LLMSingleActionAgent, AgentExecutor
from langchain_community.llms import Tongyi

def create_fund_qa_agent():
    # 建议 temperature 设低,保证工具调用的准确性
    llm = Tongyi(model_name="qwen3-max", temperature=0.1) 
    
    # ... 初始化 tool, prompt, parser ...

    llm_chain = LLMChain(llm=llm, prompt=agent_prompt)

    agent = LLMSingleActionAgent(
        llm_chain=llm_chain,
        output_parser=output_parser,
        stop=["\nObservation:"],  # 遇到 Observation 立即停止生成
        allowed_tools=[tool.name for tool in tools],
    )

    return AgentExecutor.from_agent_and_tools(
        agent=agent, 
        tools=tools, 
        verbose=True, # 开启日志,观察思考过程
        handle_parsing_errors=True
    )

3. 运行效果展示

当我们询问:"合格投资者需要满足什么条件? "时,控制台输出如下(verbose模式):

可以看到,Agent 成功地:

  1. 理解意图:分析出需要查询定义。
  2. 调用工具:准确使用了"关键词搜索"。
  3. 整合答案:根据 Observation 生成了最终回复。

4. 总结与优化思路

通过本文的代码,我们构建了一个鲁棒性较强的垂直领域问答助手。相比于简单的 RAG,Agent 能够处理更复杂的逻辑,例如"先查A,再查B,最后对比"。

本次代码的 3 大优化点总结:

  1. Prompt 强化 :通过 intermediate_steps 的手动拼接,强制模型进入思考状态,减少卡壳。
  2. Parser 兜底:解决了模型"不按格式输出"导致的程序崩溃问题,提升了用户体验。
  3. Stop Token:有效防止了模型自问自答的幻觉问题。

后续扩展方向:

  • 接入真实的向量数据库(如 Milvus, Chroma)。
  • 增加更多工具(如计算器、实时汇率查询)。
  • 将 LangChain Classic 迁移至最新的 langchain.agents.create_react_agent 接口。

5. 完整代码

python 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
私募基金运作指引问答助手 - 反应式智能体实现 (优化版)
"""
import os
import re
from typing import List, Dict, Any, Union

# 注意:根据您的环境,请保留您原本能跑通的导入路径
# 如果是较新版本的 LangChain,建议逐渐迁移到 langchain.agents.create_react_agent
from langchain_classic.agents import AgentOutputParser, LLMSingleActionAgent, AgentExecutor
from langchain_classic.chains.llm import LLMChain
from langchain_community.llms import Tongyi
from langchain_core.agents import AgentFinish, AgentAction
from langchain_core.language_models import BaseLLM
from langchain_core.prompts import PromptTemplate, StringPromptTemplate
from langchain_core.tools import Tool

# 通义千问API密钥
DASHSCOPE_API_KEY = os.environ.get("DASHSCOPE_API_KEY")

# --- 数据定义保持不变 ---
FUND_RULES_DB = [
    {
        "id": "rule001",
        "category": "设立与募集",
        "question": "私募基金的合格投资者标准是什么?",
        "answer": "合格投资者是指具备相应风险识别能力和风险承担能力,投资于单只私募基金的金额不低于100万元且符合下列条件之一的单位和个人:\n1. 净资产不低于1000万元的单位\n2. 金融资产不低于300万元或者最近三年个人年均收入不低于50万元的个人"
    },
    {
        "id": "rule002",
        "category": "设立与募集",
        "question": "私募基金的最低募集规模要求是多少?",
        "answer": "私募证券投资基金的最低募集规模不得低于人民币1000万元。对于私募股权基金、创业投资基金等其他类型的私募基金,监管规定更加灵活,通常需符合基金合同的约定。"
    },
    {
        "id": "rule014",
        "category": "监管规定",
        "question": "私募基金管理人的风险准备金要求是什么?",
        "answer": "私募证券基金管理人应当按照管理费收入的10%计提风险准备金,主要用于赔偿因管理人违法违规、违反基金合同、操作错误等给基金财产或者投资者造成的损失。"
    }
]

# --- Prompt 模板定义 (这里不需要变动太大,主要逻辑在 Agent Prompt) ---
CONTEXT_QA_TMPL = """
你是私募基金问答助手。请根据以下信息回答问题:

信息:{context}
问题:{query}
"""
CONTEXT_QA_PROMPT = PromptTemplate(
    input_variables=["query", "context"],
    template=CONTEXT_QA_TMPL,
)

OUTSIDE_KNOWLEDGE_TMPL = """
你是私募基金问答助手。用户的问题是关于私募基金的,但我们的知识库中没有直接相关的信息。
请礼貌地告知用户知识库中暂无此详细信息,并根据通用知识简要回答。

用户问题:{query}
缺失的知识主题:{missing_topic}
"""
OUTSIDE_KNOWLEDGE_PROMPT = PromptTemplate(
    input_variables=["query", "missing_topic"],
    template=OUTSIDE_KNOWLEDGE_TMPL,
)


# --- 工具类保持不变 ---
class FundRulesDataSource:
    def __init__(self, llm: BaseLLM):
        self.llm = llm
        self.rules_db = FUND_RULES_DB

    def search_rules_by_keywords(self, keywords: str) -> str:
        """通过关键词搜索相关私募基金规则"""
        keywords = keywords.strip().lower()
        keyword_list = re.split(r'[,,\s]+', keywords)
        matched_rules = []
        for rule in self.rules_db:
            rule_text = (rule["category"] + " " + rule["question"]).lower()
            match_count = sum(1 for kw in keyword_list if kw in rule_text)
            if match_count > 0:
                matched_rules.append((rule, match_count))
        matched_rules.sort(key=lambda x: x[1], reverse=True)
        if not matched_rules:
            return "未找到与关键词相关的规则。"
        result = []
        for rule, _ in matched_rules[:2]:
            result.append(f"类别: {rule['category']}\n问题: {rule['question']}\n答案: {rule['answer']}")
        return "\n\n".join(result)

    def search_rules_by_category(self, category: str) -> str:
        """根据规则类别查询私募基金规则"""
        category = category.strip()
        matched_rules = []
        for rule in self.rules_db:
            if category.lower() in rule["category"].lower():
                matched_rules.append(rule)
        if not matched_rules:
            return f"未找到类别为 '{category}' 的规则。"
        result = []
        for rule in matched_rules:
            result.append(f"问题: {rule['question']}\n答案: {rule['answer']}")
        return "\n\n".join(result)

    def answer_question(self, query: str) -> str:
        """直接回答用户关于私募基金的问题"""
        query = query.strip()
        best_rule = None
        best_score = 0
        for rule in self.rules_db:
            query_words = set(query.lower().split())
            rule_words = set((rule["question"] + " " + rule["category"]).lower().split())
            common_words = query_words.intersection(rule_words)
            score = len(common_words) / max(1, len(query_words))
            if score > best_score:
                best_score = score
                best_rule = rule

        if best_score < 0.2 or best_rule is None:
            missing_topic = self._identify_missing_topic(query)
            prompt = OUTSIDE_KNOWLEDGE_PROMPT.format(query=query, missing_topic=missing_topic)
            response = self.llm(prompt)
            return f"知识库无直接记录。补充回答:{response}"

        context = best_rule["answer"]
        prompt = CONTEXT_QA_PROMPT.format(query=query, context=context)
        return self.llm(prompt)

    def _identify_missing_topic(self, query: str) -> str:
        return "私募基金相关规则"


# --- 核心修改部分开始 ---

# 1. 优化 Agent Prompt:增加对 Final Answer 的明确指引
AGENT_TMPL = """你是一个专业的私募基金问答助手。你的任务是利用给定工具回答用户问题。

你可以使用以下工具:

{tools}

请严格遵循以下思考流程(Thought/Action/Observation/Final Answer):

1. **Thought**: 思考用户的问题需要什么信息,我应该采取什么行动?
2. **Action**: 选择一个合适的工具,工具名称必须是 [{tool_names}] 之一。
3. **Action Input**: 输入工具所需的参数。
4. **Observation**: 工具返回的结果(这一步由系统自动完成)。
5. **Thought**: 观察结果。如果结果足以回答问题,请直接给出最终答案。
6. **Final Answer**: 给用户的最终回答。

**重要提示**:
- 当你得到 "Observation" 后,请立即思考并给出 "Final Answer"。
- 不要编造工具名称。
- 如果知识库没有相关信息,请在 Final Answer 中诚实说明。

--- 示例 ---
Question: 私募基金的合格投资者标准是什么?
Thought: 我需要查找关于合格投资者的定义。
Action: 关键词搜索
Action Input: 合格投资者标准
Observation: 合格投资者是指...(工具返回内容)
Thought: 我已经获得了关于合格投资者的信息,可以回答用户了。
Final Answer: 私募基金的合格投资者标准是...
--- 示例结束 ---

开始回答:

Question: {input}
{agent_scratchpad}"""


# 2. 优化 Prompt Template:正确处理 scratchpad
class CustomPromptTemplate(StringPromptTemplate):
    template: str
    tools: List[Tool]

    def format(self, **kwargs) -> str:
        intermediate_steps = kwargs.pop("intermediate_steps")
        thoughts = ""
        for action, observation in intermediate_steps:
            thoughts += action.log
            # 关键修改:在 Observation 后加上 \nThought: 引导模型进入思考
            thoughts += f"\nObservation: {observation}\nThought: "

        kwargs["agent_scratchpad"] = thoughts
        kwargs["tools"] = "\n".join([f"{tool.name}: {tool.description}" for tool in self.tools])
        kwargs["tool_names"] = ", ".join([tool.name for tool in self.tools])

        return self.template.format(**kwargs)


# 3. 增强 Output Parser:增加容错逻辑
class CustomOutputParser(AgentOutputParser):
    def parse(self, llm_output: str) -> Union[AgentAction, AgentFinish]:
        # 清理输出中的多余空行
        llm_output = llm_output.strip()

        # 情况1:模型已经给出了最终答案
        if "Final Answer:" in llm_output:
            return AgentFinish(
                return_values={"output": llm_output.split("Final Answer:")[-1].strip()},
                log=llm_output,
            )

        # 情况2:解析 Action 和 Action Input
        regex = r"Action\s*\d*\s*:(.*?)\nAction\s*\d*\s*Input\s*\d*\s*:[\s]*(.*)"
        match = re.search(regex, llm_output, re.DOTALL)

        if match:
            action = match.group(1).strip()
            action_input = match.group(2).strip(' "')
            return AgentAction(tool=action, tool_input=action_input, log=llm_output)

        # 情况3 [关键修复]:模型没有严格遵循格式,但已经输出了文本内容(通常发生在得到 observation 后)
        # 如果模型没有输出 Action,我们假设它是在尝试直接回答
        if not match and "Action:" not in llm_output:
            # 视为最终答案
            return AgentFinish(
                return_values={"output": llm_output},
                log=f"Fallback parsing: treated as final answer. Output: {llm_output}"
            )

        raise ValueError(f"无法解析 LLM 输出: `{llm_output}`")


def create_fund_qa_agent():
    # 使用温度为0,增加输出的确定性
    llm = Tongyi(model_name="qwen3-max", dashscope_api_key=DASHSCOPE_API_KEY, temperature=0.1)

    fund_rules_source = FundRulesDataSource(llm)

    tools = [
        Tool(
            name="关键词搜索",
            func=fund_rules_source.search_rules_by_keywords,
            description="通过关键词搜索规则。输入例如:'合格投资者', '募集规模'",
        ),
        Tool(
            name="类别查询",
            func=fund_rules_source.search_rules_by_category,
            description="查询特定类别。输入必须是:'设立与募集' 或 '监管规定'",
        ),
        Tool(
            name="回答问题",
            func=fund_rules_source.answer_question,
            description="当需要综合分析或直接回答时使用。输入完整问题。",
        ),
    ]

    agent_prompt = CustomPromptTemplate(
        template=AGENT_TMPL,
        tools=tools,
        input_variables=["input", "intermediate_steps"],
    )

    output_parser = CustomOutputParser()

    # 绑定 stop 参数,防止模型替工具生成 Observation
    llm_chain = LLMChain(llm=llm, prompt=agent_prompt)

    tool_names = [tool.name for tool in tools]

    agent = LLMSingleActionAgent(
        llm_chain=llm_chain,
        output_parser=output_parser,
        stop=["\nObservation:"],
        allowed_tools=tool_names,
    )

    # 增加 max_iterations 防止死循环
    agent_executor = AgentExecutor.from_agent_and_tools(
        agent=agent,
        tools=tools,
        verbose=True,
        max_iterations=5,
        handle_parsing_errors=True  # 允许 LangChain 自动处理部分解析错误
    )

    return agent_executor


if __name__ == "__main__":
    if not DASHSCOPE_API_KEY:
        print("错误:请设置 DASHSCOPE_API_KEY 环境变量")
        exit(1)

    fund_qa_agent = create_fund_qa_agent()

    print("=== 私募基金运作指引问答助手(优化版)===\n")

    # 测试案例
    test_questions = [
        "合格投资者需要满足什么条件?",
        # "风险准备金是多少?", # 可以取消注释测试更多
    ]

    # 交互模式
    while True:
        try:
            user_input = input("\n请输入您的问题 (输入 'q' 退出):")
            if user_input.lower() in ['q', 'quit', 'exit']:
                break

            # 使用 invoke 替代 run (LangChain 新版推荐)
            if hasattr(fund_qa_agent, "invoke"):
                response = fund_qa_agent.invoke({"input": user_input})
                print(f"\n>> 回答: {response['output']}\n")
            else:
                response = fund_qa_agent.run(user_input)
                print(f"\n>> 回答: {response}\n")

        except Exception as e:
            print(f"执行出错: {e}")
相关推荐
我很哇塞耶1 小时前
AWS AgentCore重磅升级,三大新功能重塑AI代理开发体验
人工智能·ai·大模型
断春风2 小时前
Java 集成 AI 大模型最佳实践:从零到一打造智能化后端
java·人工智能·ai
Zzzzzxl_2 小时前
互联网大厂Java/Agent面试实战:Spring Boot、JVM、微服务、Kafka与AI Agent场景问答
java·jvm·spring boot·redis·ai·kafka·microservices
蓝耘智算12 小时前
GPU算力租赁与算力云平台选型指南:从需求匹配到成本优化的实战思路
大数据·人工智能·ai·gpu算力·蓝耘
aLong@201613 小时前
iflow通过hooks增加提醒
ai·aigc·agi
boboo_2000_014 小时前
基于SpringBoot+Langchain4j的AI机票预订系统
spring cloud·微服务·云原生·langchain
Elastic 中国社区官方博客14 小时前
Elasticsearch 中使用 NVIDIA cuVS 实现最高快 12 倍的向量索引速度:GPU 加速第 2 章
大数据·人工智能·elasticsearch·搜索引擎·ai·全文检索·数据库架构
小糖学代码15 小时前
LLM系列:1.python入门:2.数值型对象
人工智能·python·ai
TechTrek15 小时前
阿里上线千问“最强学习模型”,中兴发布EmbodiedBrain具身智能模型,OpenAI提出忏悔训练新方法
具身智能·mistral·千问