AI Agent 系统学习笔记:从五步闭环到报销单实战设计

在这里插入图片描述

这篇笔记记录了我从零开始理解 AI Agent 系统的完整学习路径。内容从一个最朴素的问题出发——“ChatGPT 和 Agent 到底差在哪”——逐步深入到架构选型、记忆设计、工具调用的可靠性问题,最后落到一道面试题的完整方案设计。适合对大模型有基本了解、想系统学习 Agent 架构的读者。


什么是 Agent:比 Chatbot 多了"手脚"

ChatGPT 这样的对话模型本质上是一个"只有嘴"的系统:你给它文字,它还你文字。它不能查数据库,不能发邮件,不能操作任何外部系统。如果你问它"帮我查一下上个月的销售额",它会告诉你"我无法访问你的数据库"——因为它没有"手"。

Agent 的核心区别在于:它在语言模型的大脑外,接上了感知环境的手脚。它能读取外部数据、调用 API、执行代码、操作文件系统,然后把执行结果喂回给模型,让模型决定下一步做什么。这个过程不是一次性的,而是一个持续运转的循环——模型不断地"思考→行动→观察结果→再思考",直到任务完成。

Anthropic 在 2024 年的 Agent 架构指南中给了一个简洁的定义:Agent 是 LLM 动态决定自己的工作流程和工具使用方式,对任务保持控制权并自主推进直到完成目标。相比之下,Workflow(工作流)是预先编排好步骤的固定流程,LLM 只在其中扮演特定节点的角色。这个区分很重要:Agent 的核心特征是自主决策,而不是简单地"调用了工具"。


五步闭环:Agent 的运转引擎

几乎所有 Agent 系统都可以拆解为五个环节的循环。理解这个循环,就理解了 Agent 的运转方式。

感知:接收并理解输入

Agent 的入口是感知。输入可能来自用户的自然语言指令,也可能是上游系统传递的结构化数据,甚至是定时触发的事件。感知层的工作是把异构的输入统一成模型能理解的文本表示。

一个容易被忽略的点:感知不只是"把输入传给模型"。当 Agent 运行在一个真实环境中(比如浏览器、文件系统、数据库),它还需要感知环境状态——当前打开了哪些文件、数据库里有哪些表、API 返回了什么结构。这些环境上下文需要被压缩成模型上下文窗口能容纳的摘要,否则模型就是在"盲人摸象"。

规划:把大目标拆成小步骤

规划是 Agent 区别于简单工具调用的关键。用户说"帮我订下周去上海的机票和酒店,再写一份行程",这句话包含三个子任务,每个子任务又有依赖关系——酒店日期取决于航班日期,行程取决于前两者的结果。

规划能力是当前 Agent 系统最薄弱的环节。LLM 在规划时容易犯两类错误:一是"贪婪式规划",只看下一步最优,忽略全局约束;二是"过度规划",生成一个冗长到无法执行的计划,中途任意一步出错就全盘崩溃。后面的架构演进部分会讲不同框架如何应对这个问题。

工具调用与执行:把意图变成动作

规划确定了"要做什么",工具调用负责"怎么做"。模型根据当前状态选择合适的工具,生成调用参数,执行引擎接收参数并调用对应的 API 或函数,返回执行结果。

这里有一个核心痛点:LLM 生成的工具调用参数经常出错。参数类型不匹配、必填字段缺失、枚举值不在合法范围内——这些问题在生产环境中会导致执行失败甚至系统崩溃。后面会专门讨论工具调用的可靠性问题。

反馈:从结果中学习

工具执行完毕后,返回的结果需要被模型消化。如果报销单提交成功,Agent 可以告诉用户"已完成";如果失败,Agent 需要分析错误原因,决定是重试、换一个工具,还是向用户求助。

反馈环节的设计决定了 Agent 的"韧性"(resilience)。一个健壮的 Agent 应该能区分"可重试的临时错误"(如网络超时)和"需要人工介入的逻辑错误"(如报销金额超出政策上限),并采取不同的恢复策略。

记忆:跨轮次的经验积累

记忆让 Agent 不只是"活在当下"。没有记忆的 Agent 每次对话都从零开始,不记得你上次告诉它你的报销政策、不记得你偏好经济舱。记忆系统让 Agent 积累用户偏好、任务模式和历史经验,逐步形成稳定的行为模式。

记忆是 Agent 系统中最复杂的组件之一,后面会展开讲它的分层设计和检索策略。

闭环运转

把五个环节串起来:

用户输入 → [感知] → 理解任务目标和环境状态
         → [规划] → 拆解为有序子任务
         → [工具调用] → 选择工具、生成参数
         → [执行] → 调用 API/函数
         → [反馈] → 分析结果,决定下一步
         → [记忆] → 存储关键信息
         → 回到 [感知],处理下一个子任务
         → 所有子任务完成 → 输出最终结果

这个循环会持续运转,直到所有子任务完成或遇到无法恢复的错误。循环的每一轮都消耗模型推理——因此轮次越多,延迟越高,成本也越高。控制循环次数是 Agent 工程优化的重要目标。


架构演进:从 ReAct 到 LangGraph

ReAct:思考与行动的交错

ReAct(Reasoning + Acting)是 2022 年 Yao 等人提出的 Agent 范式,也是目前应用最广的 Agent 架构。核心思想极其简洁:让模型在"思考"和"行动"之间交替进行。

一个典型的 ReAct 循环:

Thought: 用户要查上季度销售额,我需要调用数据库查询工具。
Action: query_database(sql="SELECT SUM(amount) FROM sales WHERE quarter='Q3'")
Observation: 结果为 1,250,000
Thought: 查到了销售额是 125 万。用户还问同比增长率,我需要查去年同期数据。
Action: query_database(sql="SELECT SUM(amount) FROM sales WHERE quarter='Q3-2024'")
Observation: 结果为 1,000,000
Thought: 去年同期 100 万,增长率 = (125-100)/100 = 25%
Answer: 上季度销售额 125 万元,同比增长 25%。

ReAct 的优点是简单直观,模型每一步都"说出自己的思考过程",便于调试和人类理解。缺点是缺乏全局规划——模型每次只看一步,面对需要前瞻的多步骤任务时容易走弯路。比如"订机票+订酒店+写行程"这种有依赖关系的任务,ReAct 可能先订了机票才发现酒店日期对不上。

Plan-and-Execute:先规划再执行

Plan-and-Execute 架构把规划和执行拆成两个阶段。先用一个 LLM 生成完整的计划(“第一步查航班,第二步根据航班日期订酒店,第三步根据前两步结果写行程”),再用另一个 LLM 逐步执行计划。

Plan:
  1. 查询下周北京到上海的航班
  2. 根据航班日期,查询上海酒店可用性
  3. 预订航班和酒店
  4. 根据预订信息生成行程文档

Execute:
  Step 1: 调用航班查询 API → 得到航班信息
  Step 2: 调用酒店查询 API → 得到酒店信息
  Step 3: 调用预订 API → 得到预订确认
  Step 4: 调用文档生成工具 → 生成行程

Plan-and-Execute 的优势是全局视野好,能处理有依赖关系的多步骤任务。劣势是计划容易僵化——如果执行过程中发现计划有误(比如航班取消了),需要重新规划,而重新规划的成本很高。现代实现通常加入"动态重规划"机制:执行器发现偏差时,把当前状态反馈给规划器,让它调整剩余计划。

AutoGPT:自主性的极端实验

AutoGPT 是 2023 年爆火的开源项目,它把 Agent 的自主性推到了极端。用户只给一个高层目标(如"帮我研究一下电动汽车市场并写一份报告"),AutoGPT 就自主分解任务、搜索网页、读写文件、生成报告,全程几乎不需要人工干预。

AutoGPT 的架构本质上是 ReAct 的自主循环版本,但加入了文件系统读写和长期记忆。它的核心机制是:模型在每一步都能访问一个"工作目录",可以把中间结果写入文件,后续步骤从文件读取,从而突破上下文窗口的限制。

AutoGPT 在演示中惊艳,但在实际使用中暴露了 Agent 的核心局限:长时间运行的自主循环容易"跑偏"——模型可能在第三步开始做与目标无关的事情,或者陷入"搜索→总结→再搜索"的死循环。这个问题至今没有完美解法,它揭示了 LLM 作为决策引擎的内在局限。

LangGraph:用图结构编排 Agent

LangGraph 是 LangChain 团队推出的 Agent 编排框架,它用有向图来描述 Agent 的工作流程。图中的每个节点是一个处理单元(可以是 LLM 推理、工具调用、条件判断),边定义了节点之间的流转关系。

LangGraph 的核心三要素:

  • State:一个类型化的共享状态字典,所有节点通过读写这个状态来通信,而不是直接传参
  • Nodes:执行具体逻辑的函数,接收当前状态,返回状态更新
  • Edges:定义节点之间的跳转关系,包括条件边(根据状态决定下一个节点)
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
import operator

class AgentState(TypedDict):
    messages: Annotated[list, operator.add]
    tool_results: list
    current_step: str

def planner(state: AgentState):
    # LLM 生成执行计划
    plan = llm.invoke("根据用户需求生成计划: " + state["messages"][-1])
    return {"current_step": plan}

def executor(state: AgentState):
    # 执行当前步骤
    result = execute_step(state["current_step"])
    return {"tool_results": [result]}

def should_continue(state: AgentState):
    if task_complete(state):
        return END
    return "planner"

# 构建图
graph = StateGraph(AgentState)
graph.add_node("planner", planner)
graph.add_node("executor", executor)
graph.add_edge("planner", "executor")
graph.add_conditional_edges("executor", should_continue)
graph.set_entry_point("planner")

app = graph.compile()

LangGraph 相比前几种架构的最大进步是可观测性和可恢复性。每个节点执行后都会写入一个检查点(checkpoint),如果某个节点失败,可以从上一个检查点恢复,不用从头跑。它还支持 human-in-the-loop——在关键节点暂停,等待人工确认后再继续。这对生产环境的 Agent 系统至关重要。

四种架构的对比

架构规划方式自主性可控性适用场景
ReAct逐步推理,无全局计划单轮问答、简单工具调用
Plan-and-Execute先规划再执行多步骤任务、有依赖关系的流程
AutoGPT完全自主循环探索性任务、研究类任务
LangGraph图结构编排,支持动态路由可配置生产环境、需要错误恢复的复杂流程

一个选型建议:原型验证用 ReAct,多步骤任务用 Plan-and-Execute,生产部署用 LangGraph。AutoGPT 式的全自主循环目前还不适合直接用于生产——它太容易跑偏,且难以调试。


记忆系统:从短时记忆到长期经验库

记忆的三个层次

人类大脑有感觉记忆、短时记忆和长时记忆的分层结构。Agent 的记忆系统也采用了类似的分层设计。

工作记忆(Working Memory)就是模型的上下文窗口。当前对话的最近几轮交互、工具调用的返回结果、当前的规划状态——这些都放在上下文窗口里,模型可以直接"看到"。工作记忆的容量受限于上下文窗口大小(如 128K tokens),超出就会被截断。

短期记忆(Short-term Memory)是跨轮次但会话内的记忆。比如用户在对话开头说"我是销售部的,工号 A1234",后面提到"按我的部门政策报销"时,Agent 需要记住这个信息。短期记忆通常通过对话摘要(summarization)来管理——当对话超过一定长度时,把早期对话压缩成摘要,腾出上下文空间。

长期记忆(Long-term Memory)是跨会话的持久化记忆。用户的偏好、历史操作记录、学到的任务模式——这些信息存储在外部数据库中,在需要时检索回来。长期记忆是 Agent 形成"人格"和"经验库"的基础。

向量数据库:语义检索的基石

长期记忆的存储和检索通常依赖向量数据库。基本流程:把每条记忆(一段对话、一个操作记录、一条用户偏好)通过 embedding 模型转成向量,存入向量数据库。检索时,把当前查询也转成向量,用余弦相似度找到最相关的记忆。

主流向量数据库的选择:

数据库特点适用场景
FAISSMeta 开源,纯库无服务端,性能极高单机部署、原型开发
Chroma轻量级,内置 embedding 功能,API 友好中小规模应用、快速集成
Qdrant支持过滤、分片、分布式部署生产环境、大规模数据
Pinecone全托管云服务,免运维团队无向量数据库运维能力

一个关键的工程问题:向量检索的语义相似不等于任务相关。"我上次报销被拒绝了"和"报销政策规定不超过 5000 元"在语义上可能很相似(都涉及报销),但在当前任务中的相关性可能完全不同。纯向量检索容易引入语义干扰——检索出一堆"看起来相关但实际无用"的记忆,浪费上下文窗口。

记忆检索的结构化改进

解决语义干扰的一个思路是给记忆加结构化索引。不是把所有记忆扔进一个向量库做纯语义检索,而是按类型分层存储:

  • 用户画像:结构化存储(部门、职级、报销偏好),用精确查询而非语义检索
  • 任务模板:存历史成功任务的执行计划,按任务类型索引
  • 经验日志:存操作记录和结果,按时间+任务类型双重索引,向量检索作为补充
class StructuredMemory:
    def __init__(self):
        self.user_profile = {}       # 结构化用户画像
        self.task_templates = {}     # 任务模板库
        self.experience_log = []     # 经验日志(向量索引)

    def store(self, memory_type, content, metadata=None):
        if memory_type == "profile":
            self.user_profile.update(content)
        elif memory_type == "template":
            task_type = metadata["task_type"]
            self.task_templates[task_type] = content
        elif memory_type == "experience":
            embedding = embed(content)
            self.experience_log.append({
                "content": content,
                "embedding": embedding,
                "metadata": metadata,
                "timestamp": datetime.now()
            })

    def retrieve(self, query, memory_type=None, top_k=5):
        if memory_type == "profile":
            return self.user_profile
        elif memory_type == "template":
            return self.task_templates.get(query.get("task_type"))
        else:
            # 向量检索 + 元数据过滤
            query_vec = embed(query["text"])
            results = vector_search(
                self.experience_log, query_vec,
                filter=lambda x: x["metadata"].items() <= query.get("filter", {}).items(),
                top_k=top_k
            )
            return results

Letta(前 MemGPT 团队)在 2026 年的实验中发现一个有趣的结论:在编程任务上,Agent 直接使用文件系统工具(读写文件)来管理记忆,有时比专门的向量数据库效果更好。原因是文件系统天然支持层级结构和命名约定,Agent 可以通过文件名和目录结构快速定位信息,而向量检索的语义匹配反而可能引入噪声。这个发现提醒我们:记忆系统的设计不应该一上来就堆向量数据库,而应该根据记忆的类型选择最合适的存储和检索方式。

上下文窗口管理

即使有了外部记忆系统,上下文窗口的管理仍然是一个核心工程问题。一个运行了 10 轮的 Agent,它的上下文里可能有:用户指令、规划状态、5 次工具调用的输入和输出、中间推理过程。这些内容加起来很容易超出窗口限制。

常见的上下文管理策略:

  • 摘要压缩:把早期的对话和工具结果压缩成一段摘要,只保留关键信息
  • 选择性遗忘:删除与当前子任务无关的中间结果(如查询结果的原始 JSON,只保留提取的关键字段)
  • 分层加载:只把当前子任务需要的记忆加载到上下文,其他记忆在外部按需检索

一个实践原则:上下文窗口里的每一行文字都应该对当前决策有贡献。如果一段信息"可能有用但不确定",与其占着上下文空间,不如放到外部记忆里按需检索。


工具调用:可靠性是第一要务

Function Calling 的基本机制

OpenAI 在 2023 年 6 月推出了 Function Calling 功能,让 LLM 能够输出结构化的工具调用请求。模型接收一组工具定义(名称、描述、参数 schema),在推理时如果判断需要使用工具,就输出一个符合 schema 的 JSON 对象。

tools = [
    {
        "type": "function",
        "function": {
            "name": "query_expense_policy",
            "description": "查询公司报销政策,返回指定费用类型的报销上限和所需材料",
            "parameters": {
                "type": "object",
                "properties": {
                    "expense_type": {
                        "type": "string",
                        "enum": ["交通", "餐饮", "住宿", "办公用品"],
                        "description": "费用类型"
                    },
                    "department": {
                        "type": "string",
                        "description": "部门名称"
                    }
                },
                "required": ["expense_type"]
            }
        }
    }
]

# 模型输出示例:
# {"name": "query_expense_policy",
#  "arguments": {"expense_type": "住宿", "department": "销售部"}}

Function Calling 的意义在于把工具调用从"提示词工程"变成了"结构化输出"。模型不再需要自由生成一段可能格式混乱的文本,而是在 schema 约束下输出规范的 JSON。这大幅提高了工具调用的可靠性。

Toolformer:让模型学会自己调工具

Toolformer(Schick et al., 2023)走了一条不同的路:它不是在推理时通过 prompt 引导模型调工具,而是在训练阶段就让模型学会"什么时候调工具、怎么生成调用参数"。

具体做法:先用模型自标注——让模型在文本中插入工具调用的候选位置,执行调用后比较结果是否有用,有用的保留作为训练数据。然后用这些自标注数据微调模型,让模型内化工具调用的能力。

Toolformer 的贡献在于证明了工具使用能力可以通过训练习得,而不只依赖 prompt 工程。但它的影响力更多在学术层面——工业界目前主要还是用 Function Calling + prompt 工程的方案。

工具调用为什么会出错

根据生产环境的故障分析,工具调用失败主要有以下几类原因:

参数格式错误:模型生成的参数类型不匹配。比如工具要求 date 字段是 ISO 格式的字符串(“2026-08-13”),模型生成了"8月13日"或"2026/08/13"。这类错误最常见,也最容易修复——通过 schema 校验在执行前拦截,返回格式错误提示让模型重新生成。

参数逻辑错误:格式正确但值不合理。比如查询日期范围是"2026-08-13 到 2026-08-10"(结束日期早于开始日期),或者报销金额填了负数。这类错误需要业务逻辑校验,schema 校验无法覆盖。

工具选择错误:模型选了错误的工具。用户要查"住宿报销上限",模型调用了"交通报销查询"工具。这类错误通常是因为工具描述写得不够清晰,或者多个工具的功能边界模糊。

级联失败:多步工具调用中,前一步的错误传播到后续步骤。第一步查询返回了错误的数据,第二步基于错误数据继续执行,错误不断放大。生产环境中这类故障最难排查,因为表面上看每一步都"成功执行了",但最终结果完全错误。

错误恢复机制

针对上述问题,成熟的 Agent 系统需要设计多层错误恢复机制。

Schema 校验层:在执行工具调用前,用 JSON Schema 校验参数格式。格式不合法时,不执行调用,而是把校验错误信息返回给模型,让模型修正后重新生成。

import jsonschema

def safe_tool_call(tool_name, arguments, tool_registry):
    tool = tool_registry[tool_name]
    schema = tool["parameters"]

    try:
        jsonschema.validate(arguments, schema)
    except jsonschema.ValidationError as e:
        # 参数校验失败,返回错误信息让模型修正
        return {
            "status": "validation_error",
            "error": f"参数校验失败: {e.message}",
            "suggestion": "请检查参数类型和必填字段,重新生成调用参数。"
        }

    # 校验通过,执行工具
    try:
        result = tool["function"](**arguments)
        return {"status": "success", "result": result}
    except Exception as e:
        return {"status": "execution_error", "error": str(e)}

重试机制:对于临时性错误(网络超时、API 限流),自动重试。重试时加入退避策略(exponential backoff),避免短时间内反复请求同一个失败的 API。重试次数应设置上限(如 3 次),超过后转为降级处理。

降级策略:当首选工具不可用时,切换到备选方案。比如主数据库查询超时,切换到缓存或只读副本;航班 API 不可用,切换到备用数据源。降级策略要求系统预先定义工具的优先级和替代关系。

人工介入:当错误无法自动恢复时(如报销金额超出政策上限、用户身份验证失败),暂停 Agent 循环,通知人工处理。LangGraph 的 human-in-the-loop 机制天然支持这种模式——在关键节点设置中断点,等待人工确认后再继续。


多 Agent 协作:分工与协调

为什么需要多 Agent

单个 Agent 处理简单任务没问题,但面对复杂任务会遇到瓶颈。一个 Agent 同时负责理解需求、查询数据、生成报告、审核合规性——上下文窗口装不下这么多信息,模型也容易在不同任务模式之间"串台"。

多 Agent 协作的思路是分而治之:把一个复杂任务拆分给多个专职 Agent,每个 Agent 只负责自己擅长的领域。就像公司里不同的部门协作完成一个项目。

常见的协作模式

主管-工人模式(Supervisor-Worker):一个主管 Agent 负责任务分解和分配,多个工人 Agent 负责执行具体子任务。主管收集工人的结果,整合后返回给用户。这种模式的优点是中心化控制,流程清晰;缺点是主管是单点瓶颈,所有信息都要经过它。

流水线模式(Pipeline):多个 Agent 按顺序处理任务,前一个的输出是后一个的输入。比如"数据查询 Agent → 数据分析 Agent → 报告生成 Agent → 合规审核 Agent"。适合流程固定的场景,但不灵活——如果第二步发现问题需要回到第一步,流水线就需要回退机制。

辩论模式(Debate):多个 Agent 对同一问题给出不同的方案,然后互相审视、辩论,最终收敛到一个更优的方案。适合需要多角度论证的决策类任务,但对话轮次多、成本高。

协作的工程挑战

多 Agent 系统引入了新的工程复杂度。最突出的是通信成本:Agent 之间传递信息需要序列化和反序列化,每次通信都消耗 token。如果 5 个 Agent 互相传递完整的上下文,token 消耗会呈乘数增长。

一个实践原则:Agent 之间只传递必要的信息,而不是完整上下文。数据查询 Agent 只需要把"查询结果摘要"传给分析 Agent,不需要把整个查询过程和原始 JSON 都传过去。这要求设计精简的消息协议,定义清楚每个 Agent 的输入输出格式。


Agent 的局限性与核心原则

三个核心局限

可靠性不足:LLM 的输出本质上是概率性的。同一个输入,模型可能这次生成正确的工具调用参数,下次就出错。在需要高可靠性的生产场景(如财务、医疗),这种不确定性是致命的。目前没有完美解法,只能通过多层校验和人工介入来兜底。

长程规划弱:LLM 的注意力机制在处理长序列时存在衰减,导致多步骤规划的能力随步骤数增加而下降。一个 10 步的计划,到第 7 步可能已经偏离了原始目标。Plan-and-Execute 的动态重规划能缓解但无法根治这个问题。

成本难以控制:Agent 的自主循环特性使得每次任务的 token 消耗不可预测。一个简单的报销单填写可能 3 轮就完成,也可能因为参数错误反复重试消耗 20 轮。成本控制需要在系统层面设置预算上限和熔断机制。

三条核心原则

人在回路(Human-in-the-loop):关键决策点必须有人工确认。Agent 可以自主完成低风险操作(如查询数据),但在涉及资金、权限、不可逆操作时(如提交报销单、发送邮件),必须暂停等待人工确认。这不是"不信任 Agent",而是承认 LLM 的不确定性,用人工判断兜底。

最小权限:Agent 能调用的工具和能访问的数据应该严格限制在任务所需的最小范围内。一个报销 Agent 不需要访问整个人力资源数据库,只需要报销相关的 API。最小权限不是"防着 Agent",而是"防着 Agent 出错时的爆炸半径"。

可观测可恢复:Agent 的每一步操作都应该有日志记录,包括模型推理过程、工具调用参数、执行结果。当出错时,能快速定位是哪一步出了问题。LangGraph 的 checkpoint 机制提供了天然的恢复点——从最后一个成功的 checkpoint 重启,而不是从头开始。


面试实战:设计一个自动填写报销单的 Agent

需求分析

报销单填写是一个典型的多步骤、多工具、需要错误恢复的 Agent 任务。用户输入可能是一段自然语言(“帮我把上周出差的机票和酒店报了”)加上一些附件(发票照片),Agent 需要完成:识别费用类型、查询报销政策、提取发票信息、校验合规性、填写表单、提交审批。

系统架构

整个 Agent 采用 Plan-and-Execute 架构,用 LangGraph 编排。原因是报销流程有明确的步骤依赖关系(先查政策再填表单),且需要错误恢复(发票不清晰时需要重新上传)和人工介入(金额超标时需要审批)。

工具集设计

工具名称功能输入输出错误场景
extract_receipt_infoOCR 识别发票信息发票图片路径费用类型、金额、日期、商家图片模糊、非发票图片
query_expense_policy查询公司报销政策费用类型、部门报销上限、所需材料、审批流程部门不存在、费用类型不支持
validate_compliance校验报销合规性费用明细、政策规则校验结果(通过/不通过+原因)规则引擎异常
fill_expense_form填写报销单字段费用明细、报销人信息表单 ID、填写状态字段格式不匹配、必填项缺失
submit_expense提交报销单审批表单 ID审批流水号、状态网络超时、审批系统不可用
query_user_info查询报销人信息工号或姓名部门、职级、银行账户用户不存在、账户信息过期

记忆模块设计

class ExpenseAgentMemory:
    """报销 Agent 的分层记忆系统"""

    def __init__(self, user_id):
        self.user_id = user_id
        # 用户画像:结构化存储,精确查询
        self.user_profile = self._load_profile(user_id)
        # 报销历史:向量索引 + 时间过滤
        self.history_store = VectorStore(collection=f"expense_history_{user_id}")
        # 政策缓存:键值存储,避免重复查询
        self.policy_cache = {}

    def _load_profile(self, user_id):
        """从 HR 系统加载用户画像"""
        return {
            "department": "销售部",
            "level": "P6",
            "bank_account": "****1234",
            "default_cost_center": "CC-2026-SALES",
            "preferences": {
                "preferred_flight_class": "经济舱",
                "preferred_hotel_level": "三星"
            }
        }

    def get_relevant_history(self, expense_type, amount):
        """检索相关的历史报销记录"""
        query = f"{expense_type} 报销 金额 {amount}"
        results = self.history_store.search(
            query=query,
            filter={"expense_type": expense_type},
            top_k=3
        )
        return results

    def cache_policy(self, expense_type, department, policy):
        """缓存报销政策查询结果"""
        key = f"{department}:{expense_type}"
        self.policy_cache[key] = policy

记忆模块分三层:

  • 用户画像层:从 HR 系统同步的部门、职级、银行账户等结构化信息。这些信息变动频率低,在会话开始时一次性加载,整个会话期间复用。用户偏好(如偏好经济舱)也存在这里。
  • 政策缓存层:报销政策的查询结果按"部门+费用类型"缓存。同一次会话中可能提交多张同类型发票,不需要每次都查政策。
  • 历史经验层:用户的历史报销记录存入向量数据库,按费用类型和金额检索。用途是当当前报销有异常(如金额远超历史平均水平)时,可以参考历史记录给出提示。

错误恢复机制

报销场景的错误恢复设计,按错误类型分层处理:

class ExpenseAgent:
    def __init__(self, memory, tools):
        self.memory = memory
        self.tools = tools
        self.max_retries = 3

    def process_receipt(self, receipt_path):
        """处理单张发票的完整流程"""

        # 步骤1:OCR 识别发票信息
        receipt_data = self._call_with_retry(
            self.tools["extract_receipt_info"],
            {"image_path": receipt_path},
            retry_on=["image_blur", "timeout"],
            fallback=self._ask_user_to_resubmit
        )
        if not receipt_data:
            return {"status": "failed", "reason": "发票识别失败,请重新上传"}

        # 步骤2:查询报销政策
        policy = self._call_with_cache(
            self.tools["query_expense_policy"],
            {
                "expense_type": receipt_data["type"],
                "department": self.memory.user_profile["department"]
            },
            cache_key=f"{self.memory.user_profile['department']}:{receipt_data['type']}"
        )

        # 步骤3:合规性校验
        compliance = self.tools["validate_compliance"](
            expense=receipt_data,
            policy=policy
        )

        if not compliance["passed"]:
            # 不可自动恢复的合规错误,转入人工
            if compliance["error_type"] == "over_limit":
                return {
                    "status": "needs_approval",
                    "reason": f"金额 {receipt_data['amount']} 超过上限 {policy['limit']},需要额外审批",
                    "expense_data": receipt_data
                }
            elif compliance["error_type"] == "missing_material":
                return {
                    "status": "needs_material",
                    "reason": f"缺少材料: {compliance['missing']}",
                    "expense_data": receipt_data
                }

        # 步骤4:填写报销单
        form_result = self._call_with_retry(
            self.tools["fill_expense_form"],
            {
                "expense_data": receipt_data,
                "user_info": self.memory.user_profile,
                "policy_id": policy["id"]
            },
            retry_on=["network_timeout"],
            fallback=None  # 填表失败无法降级,直接报错
        )

        # 步骤5:提交审批
        submit_result = self._call_with_retry(
            self.tools["submit_expense"],
            {"form_id": form_result["form_id"]},
            retry_on=["network_timeout", "service_unavailable"],
            max_retries=5,  # 提交是关键操作,多给几次重试
            fallback=self._save_as_draft  # 提交失败先存草稿
        )

        return submit_result

    def _call_with_retry(self, tool, args, retry_on, max_retries=None,
                         fallback=None):
        """带重试和降级的工具调用"""
        retries = max_retries or self.max_retries
        for attempt in range(retries):
            result = tool(**args)
            if result["status"] == "success":
                return result["data"]

            if result.get("error_type") in retry_on:
                wait = 2 ** attempt  # 指数退避
                time.sleep(wait)
                continue
            else:
                # 不可重试的错误
                break

        # 重试耗尽,尝试降级
        if fallback:
            return fallback(args)
        return None

    def _save_as_draft(self, args):
        """降级策略:提交失败时保存为草稿"""
        draft_id = self.tools["save_draft"](**args)
        return {
            "status": "saved_as_draft",
            "draft_id": draft_id,
            "message": "审批系统暂时不可用,已保存为草稿,稍后可手动提交"
        }

    def _ask_user_to_resubmit(self, args):
        """降级策略:OCR 失败时请求用户重新上传"""
        return None  # 返回 None 触发上层处理

错误恢复策略总结

错误类型恢复策略是否自动
OCR 图片模糊请求用户重新上传清晰照片否(需用户操作)
政策查询超时重试 3 次,仍失败则使用缓存的上次查询结果
金额超标暂停,标记为"需要额外审批",通知用户否(需人工审批)
必填材料缺失暂停,通知用户补充材料否(需用户操作)
表单填写格式错误返回校验错误信息,让模型重新生成参数
审批系统不可用重试 5 次,仍失败则保存为草稿是(降级为草稿)
网络超时指数退避重试

完整执行流程

用一段模拟交互来展示 Agent 的完整运转:

用户:帮我把上周去上海出差的机票和酒店发票报了,发票照片在附件里。

Agent 感知:
  - 用户意图:报销差旅费用
  - 费用类型:交通(机票)+ 住宿(酒店)
  - 输入附件:2 张发票图片
  - 从记忆加载用户画像:销售部,P6,偏好经济舱

Agent 规划:
  1. OCR 识别两张发票
  2. 查询销售部的交通和住宿报销政策
  3. 校验每张发票的合规性
  4. 填写报销单
  5. 提交审批

Agent 执行 Step 1(OCR 识别):
  → extract_receipt_info(image_path="receipt_1.jpg")
  → 结果:{"type": "交通", "amount": 1280, "date": "2026-08-05",
           "merchant": "东方航空", "route": "北京-上海"}

  → extract_receipt_info(image_path="receipt_2.jpg")
  → 结果:{"type": "住宿", "amount": 680, "date": "2026-08-05",
           "merchant": "全季酒店", "nights": 2}

Agent 执行 Step 2(查询政策):
  → query_expense_policy(expense_type="交通", department="销售部")
  → 结果:{"limit": 2000, "required_materials": ["发票", "行程单"]}
  → query_expense_policy(expense_type="住宿", department="销售部")
  → 结果:{"limit": 500, "required_materials": ["发票"]}

Agent 执行 Step 3(合规性校验):
  → validate_compliance(expense=机票, policy=交通政策)
  → 结果:{"passed": true}
  → validate_compliance(expense=酒店, policy=住宿政策)
  → 结果:{"passed": false, "error_type": "over_limit",
           "reason": "金额 680 超过上限 500"}

Agent 错误恢复:
  - 酒店报销超标,触发人工介入
  - 检索历史记忆:用户上次上海出差住宿报销 480 元
  - 生成提示:"您的酒店发票金额 680 元超过销售部住宿上限 500 元。
              上次同类出差报销金额为 480 元。
              需要额外审批,是否继续提交?"

用户:继续提交吧,这次酒店比较贵。

Agent 执行 Step 4-5:
  - 填写报销单(机票部分正常填写,酒店部分标记"需额外审批")
  - 提交审批系统
  → 结果:{"status": "submitted", "tracking_id": "EXP-2026-0813-001",
           "note": "住宿部分需经理额外审批"}

Agent 输出:
  报销单已提交,流水号 EXP-2026-0813-001。
  - 机票 1280 元:正常审批流程
  - 酒店 680 元:已标记为超标,需经理额外审批
  预计 3 个工作日内出结果。

学习总结

回顾整个 Agent 系统的知识脉络,五个关键点值得记住:

Agent = LLM + 工具 + 记忆 + 自主循环。Agent 不是一种新模型,而是一种系统架构。它把 LLM 的推理能力、外部工具的执行能力、记忆系统的经验积累能力组合起来,形成一个能自主完成任务的闭环系统。任何一个环节薄弱都会影响整体效果。

五步闭环是骨架。感知→规划→工具调用→执行→反馈→记忆,这个循环是所有 Agent 系统的基本运转方式。不同架构(ReAct、Plan-and-Execute、LangGraph)本质上是对这个循环的不同编排方式。

可靠性是生产落地的最大障碍。工具调用参数出错、规划偏离目标、级联失败——这些问题在 demo 里看不出来,到生产环境就会暴露。多层错误恢复机制(schema 校验→重试→降级→人工介入)是必须的,不是可选的。

记忆设计决定 Agent 的上限。一个没有长期记忆的 Agent 永远是"新手"。记忆系统的核心挑战不是存储(向量数据库已经很成熟),而是检索质量——如何在海量记忆中精准找到对当前任务有用的信息,而不被语义干扰淹没。结构化索引 + 向量检索的混合方案是目前最务实的选择。

人在回路是底线。在 LLM 的可靠性达到足够高之前,完全自主的 Agent 不适合用于高风险场景。关键决策点保留人工确认,不是技术退步,而是工程上对不确定性的正确管理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值