AI Agent 开发实战:从零构建可用的 Agent 工作流 — 核心在于”把大模型的思考能力装进可控的流水线”

2026-05-03 20:46 AI Agent 开发实战:从零构建可用的 Agent 工作流 — 核心在于”把大模型的思考能力装进可控的流水线”已关闭评论

AI Agent 开发实战:从零构建可用的 Agent 工作流 — 核心在于"把大模型的思考能力装进可控的流水线"

我在为公司搭建内部客服助手时发现,直接调 GPT API 根本不可用——模型自由发挥太多,输出不稳定,上下文一长就失忆。最终落地的是一个基于 ReAct 模式 + 工具注册 + 状态管理 的 Agent 框架,这篇文章完整记录了从方案选型到生产部署的全过程。

问题是什么

年初接到一个需求:给公司客服团队做一个 AI 助手,能根据知识库回答用户问题、查询订单状态、发起退款流程。听起来不就是调一下 ChatGPT API 吗?真做起来才发现三个核心痛点:

  • **模型输出不可控**:同样的问题,今天答 A 明天答 B,客服主管直接说"这没法用"
  • **无法执行操作**:LLM 只能生成文本,没法真正去查数据库、调订单接口
  • **上下文窗口有限**:聊到第 5 轮就开始"失忆",记不住之前确认过的用户信息
  • 我需要一个架构,让 LLM 的"思考"有边界、可观测、能落地。

    解决思路

    调研了三个主流方案:

    方案 核心思路 复杂度 适合场景
    LangChain 封装好的 Agent 框架,自带工具链 中高 快速原型,但出问题难排查
    Semantic Kernel 微软的 AI 编排框架 .NET 生态友好,Python 支持一般
    自建 ReAct 循环 自己写 Prompt + Tool Registry + Loop 理解原理,完全可控

    我选了 自建 ReAct 循环。原因很简单:LangChain 的 Agent 是个黑盒,出了问题你连它在"想什么"都不知道。自建虽然要多写几百行代码,但每个环节的可观测性完全掌握在自己手里。

    核心思路就三句话:

  • LLM 负责"思考"——推理出下一步要做什么
  • Tool Registry 负责"执行"——把思考变成实际动作
  • Loop Controller 负责"串联"——决定什么时候继续、什么时候停止
  • 操作步骤

    步骤 1:设计 Tool Registry — Agent 的"手和脚"

    Agent 要能干活,必须有工具。我先定义了一套统一的工具接口:

    from abc import ABC, abstractmethod
    from typing import Dict, Any
    from pydantic import BaseModel
    
    class ToolSpec(BaseModel):
        """工具声明——告诉 LLM 这个工具是干什么的"""
        name: str
        description: str
        parameters: Dict[str, Any]  # JSON Schema 格式
    
    class Tool(ABC):
        """工具的基类"""
        spec: ToolSpec
        
        @abstractmethod
        def run(self, **kwargs) -> str:
            """执行工具,返回文本结果给 LLM"""
            pass
    

    每个工具都包含两部分:声明(告诉 LLM 这个工具的存在和用法)和实现(实际干活)。比如一个查订单的工具:

    class QueryOrderTool(Tool):
        spec = ToolSpec(
            name="query_order",
            description="根据订单号查询订单状态和详细信息",
            parameters={
                "type": "object",
                "properties": {
                    "order_id": {"type": "string", "description": "订单号"}
                },
                "required": ["order_id"]
            }
        )
        
        def run(self, order_id: str) -> str:
            # 实际调用订单 API
            data = requests.get(f"https://api.xxx.com/orders/{order_id}")
            return f"订单 {order_id} 状态: {data['status']}, 金额: {data['amount']}"
    

    注意: ToolSpec 的 parameters 一定要用 JSON Schema 格式。我一开始用自由文本描述,结果 LLM 经常传错参数类型,改成严格 schema 后问题解决了 90%。

    步骤 2:构建 System Prompt — Agent 的"大脑说明书"

    这是整个 Agent 最核心的部分。System Prompt 不是写一段话告诉 LLM"你要友好",而是要给它一套可执行的行动规则

    SYSTEM_PROMPT = """你是一个客服助手。请严格按照以下流程工作:
    
    ## 可用工具
    {tool_descriptions}
    
    ## 工作模式
    每次回复必须遵循以下格式:
    
    问题分析:<简述你对用户问题的分析>
    思考:<根据当前信息,决定下一步动作>
    动作:<调用的工具名>
    参数:<JSON 格式的参数>
    ---
    
    如果你已经得到最终答案,或者用户的问题不需要调用工具,直接输出:
    最终回答:<你的回答>
    
    ## 关键规则
    1. 每次只调用一个工具,等待结果后再决定下一步
    2. 如果工具返回错误,尝试换个参数重新调用,最多重试 2 次
    3. 如果用户要求查询隐私信息(密码、身份证号等),拒绝并说明原因
    4. 当上下文超过 5 轮对话时,主动总结关键信息"""
    

    {tool_descriptions} 是动态生成的,遍历所有注册的工具生成统一的描述文本:

    def _build_tool_descriptions(tools: Dict[str, Tool]) -> str:
        lines = []
        for name, tool in tools.items():
            s = tool.spec
            params_desc = "
    ".join(
                f"  - {p}: {info.get('description', '')}"
                for p, info in s.parameters.get("properties", {}).items()
            )
            lines.append(f"### {s.name}
    {s.description}
    参数:
    {params_desc}")
        return "
    
    ".join(lines)
    

    步骤 3:实现 ReAct Loop — Agent 的"引擎"

    核心循环就是一个 while True:把当前消息拼进上下文,让 LLM 生成回复,解析回复内容,执行工具,把结果追加回去,重复。

    class Agent:
        def __init__(self, tools: List[Tool], model: str = "gpt-4"):
            self.tools = {t.spec.name: t for t in tools}
            self.model = model
            self.messages = []
        
        def run(self, user_input: str) -> str:
            # 初始化消息
            self.messages = [
                {"role": "system", "content": SYSTEM_PROMPT.format(
                    tool_descriptions=_build_tool_descriptions(self.tools)
                )},
                {"role": "user", "content": user_input}
            ]
            
            max_steps = 10  # 防止无限循环
            for step in range(max_steps):
                # 1. 调用 LLM
                response = self._call_llm(self.messages)
                content = response.choices[0].message.content
                
                # 2. 检查是否最终回答
                if content.startswith("最终回答:"):
                    return content.replace("最终回答:", "").strip()
                
                # 3. 解析工具调用
                action = self._parse_action(content)
                if action is None:
                    # LLM 输出格式不对,给个纠正提示
                    self.messages.append({
                        "role": "user", 
                        "content": "格式错误,请严格按照 '动作:<工具名>
    参数:<JSON>' 格式输出"
                    })
                    continue
                
                # 4. 执行工具
                try:
                    tool = self.tools[action["name"]]
                    result = tool.run(**action["parameters"])
                except Exception as e:
                    result = f"工具执行出错: {str(e)}"
                
                # 5. 把结果追加到对话中
                self.messages.append({
                    "role": "user",
                    "content": f"工具 {action['name']} 返回结果:
    {result}
    
    请根据结果继续。"
                })
            
            return "抱歉,处理超时,请稍后重试。"
    

    踩坑记录: 我一开始把工具结果用 role: "tool" 塞进去,结果 GPT-4 在某些版本对这个 role 的支持不一致。最后改用 role: "user" + 明确的前缀标记,稳定多了。

    步骤 4:增加状态管理 — 解决"失忆"问题

    纯靠 Prompt 的 Agent 会在长对话中丢失关键信息。我加了一个显式的 Memory 模块:

    class ConversationMemory:
        def __init__(self, max_tokens: int = 3000):
            self.max_tokens = max_tokens
            self.summary = ""
            self.key_info = {}  # 关键信息:用户ID、订单号等
            
        def extract_key_info(self, text: str):
            """从对话中提取关键信息"""
            # 简单的规则提取,生产环境可以用 NER 模型
            patterns = {
                "user_id": r"用户[I云号]?[::]\s*(\w+)",
                "order_id": r"订单[号]?[::]\s*(\w+)",
                "phone": r"1[3-9]\d{9}",
            }
            for key, pattern in patterns.items():
                match = re.search(pattern, text)
                if match:
                    self.key_info[key] = match.group(1)
        
        def build_context(self) -> str:
            """生成压缩后的上下文"""
            parts = []
            if self.summary:
                parts.append(f"对话摘要({self.summary})")
            if self.key_info:
                info_str = ";".join(f"{k}={v}" for k, v in self.key_info.items())
                parts.append(f"已确认信息:{info_str}")
            return "
    ".join(parts)
    

    然后在 Agent Loop 里,每 3 轮调用一次 LLM 做摘要压缩:

    if step > 0 and step % 3 == 0:
        summary_prompt = f"请用一句话总结对话经过,提取关键信息:
    {self.messages}"
        summary = self._call_llm_simple(summary_prompt)
        memory.summary = summary
        memory.extract_key_info(summary)
        
        # 把旧消息压缩,只保留最近 2 轮和摘要
        self.messages = self.messages[:1] + [  # system prompt
            {"role": "user", "content": f"【对话摘要】{memory.build_context()}"}
        ] + self.messages[-4:]  # 最近两轮对话
    

    步骤 5:加可观测性 — 让 Agent 的思考过程可见

    客服主管不信任 AI 的原因是"我不知道它怎么得出这个结论的"。我加了一个简单的 Thought Logger:

    class ThoughtLogger:
        def __init__(self):
            self.steps = []
        
        def log(self, step: int, action: str, reasoning: str, result: str):
            self.steps.append({
                "step": step,
                "action": action,
                "reasoning": reasoning,
                "result": result[:200],  # 截断过长内容
                "timestamp": datetime.now().isoformat()
            })
        
        def to_html(self) -> str:
            """生成可展示的推理过程"""
            html = "<div class='thought-log'>"
            for s in self.steps:
                html += f"""
                <div class='step'>
                    <div class='step-header'>Step {s['step']}: {s['action']}</div>
                    <div class='step-reasoning'>🤔 {s['reasoning']}</div>
                    <div class='step-result'>✅ {s['result']}</div>
                </div>
                """
            html += "</div>"
            return html
    

    把这个 HTML 嵌入到客服工作台的"推理过程"折叠面板里,主管点开就能看到 AI 的每一步推理和工具调用结果。上线后信任度从 40% 提升到了 85%。

    结果与总结

    这个 Agent 已经运行了 3 个月,处理了约 5000 次客服会话。一些关键数据:

  • **首次回答准确率**:78%(纯 RAG 基线是 52%)
  • **平均处理轮次**:2.3 轮(包括工具调用)
  • **超时率**:3.2%(主要是 LLM API 延迟导致)
  • **用户满意度**:4.2/5(客服人工评分)
  • 最大的坑有三个

  • **Tool 返回格式要极致简洁**:LLM 的注意力有限,工具返回一长串 JSON 它就"看花了眼"。我统一把所有工具返回控制在 200 字以内,超过的自动摘要
  • **System Prompt 必须做版本管理**:改一次 Prompt 可能引入 regression。我现在所有 Prompt 都走 Git + A/B 测试
  • **兜底策略不能少**:无论如何,Agent 都可能跑偏。我加了一个"人工介入"按钮,客服可以一键接管对话,接管后 Agent 的整个上下文完整展示给人工
  • 延伸思考

    这个架构虽然够用,但还有几个方向可以继续优化:

  • **多 Agent 协作**:当前是单 Agent 处理所有事情。可以拆成 Router Agent(分类)+ Specialist Agent(执行)+ Reviewer Agent(审核),类似一个 AI 版的微服务架构
  • **工具调用并行化**:如果 LLM 判断需要同时查订单和查物流,可以一次返回多个工具调用,用 asyncio 并发执行
  • **基于反馈的自动优化**:收集客服对 AI 回答的修正,自动构造 few-shot 样例,定期微调 Prompt。这样就形成了"越用越好"的正循环
  • **流式输出**:当前是等整个 Loop 跑完才返回结果。改成 SSE 流式输出,每步推理结果实时推到前端,用户体验会好很多
  • 构建 Agent 这件事,说到底不是"让 AI 替人做决定",而是把 AI 的能力嵌入到人类的工作流中,让 AI 做执行、人类做决策。把握好这个原则,架构就不会走偏。

    你可能感兴趣的文章

    来源:每日教程每日一例,深入学习实用技术教程,关注公众号TeachCourse
    转载请注明出处: https://teachcourse.cn/4041.html ,谢谢支持!

    资源分享

    分类:Android 标签:
    python遍历文件夹下所有图片 python遍历文件夹下所有图片
    Android面试笔记六:租租车 Android面试笔记六:租租车
    Android常用基本控件 Android常用基本控件
    Android开发工程师创建项目需要掌握的Git命令 Android开发工程师创建项目需要

    评论已关闭!