1. 项目概述与核心价值

最近在折腾一些自动化任务和数据处理流程时,我一直在寻找一种既能保持代码简洁,又能实现复杂状态管理的解决方案。传统的基于状态的代理(Agent)模式,虽然功能强大,但往往伴随着沉重的框架依赖和复杂的生命周期管理,调试起来也颇为头疼。直到我遇到了 rerecoy123/statelessagent 这个项目,它提出了一种截然不同的思路: 无状态代理

简单来说, statelessagent 是一个轻量级的库或框架(具体取决于你的使用方式),它的核心思想是将代理的逻辑设计为纯函数。这意味着,代理的每一次执行都不依赖于内部隐藏的状态,所有的输入(包括历史上下文、当前指令、环境信息)都通过参数显式传入,所有的输出(包括决策、动作、新的上下文)都通过返回值显式传出。这种模式听起来似乎限制了能力,但实际用下来,你会发现它在 可测试性、可组合性、可维护性 方面带来了质的飞跃。它特别适合那些需要清晰数据流、易于调试和需要将复杂流程拆解为独立步骤的场景,比如聊天机器人对话管理、数据处理流水线、自动化决策引擎等。

如果你也厌倦了在复杂的类继承和状态机中挣扎,或者希望你的自动化逻辑能像乐高积木一样自由拼接和测试,那么这个项目值得你花时间深入了解。接下来,我将结合我的实际使用经验,从设计思路到具体实现,为你完整拆解这个项目。

2. 核心设计理念与架构拆解

2.1 什么是“无状态”代理?

在深入代码之前,我们必须先厘清“无状态”在这里的确切含义。这并非指代理完全不能处理“状态”,而是指代理 自身不维护状态

传统有状态代理的典型模式 :我们通常会定义一个 Agent 类,它内部有 self.memory self.history self.context 等属性。当调用 agent.run(prompt) 时,方法内部会读取和修改这些属性。状态被封装在对象实例内部,方法的输出不仅依赖于输入,还严重依赖于对象当前不可见的内在状态。这带来了几个问题:

  1. 难以测试 :要测试 run 方法,你必须先精确地设置好对象内部的各种状态,测试用例的准备工作繁琐。
  2. 难以推理 :同样的输入,因为内部状态不同,可能产生完全不同的输出,这增加了调试的复杂度。
  3. 难以组合 :一个有状态的 AgentA 和有状态的 AgentB 很难安全地组合在一起,因为你不清楚组合时它们内部的状态会如何相互干扰。

statelessagent 的无状态模式 :在这里,一个代理被定义为一个函数(或可调用对象),其签名类似于: (state, input) -> (new_state, output) 。或者更通用地: (context, ...) -> new_context 。其中 state context 包含了完成任务所需的全部信息(如对话历史、临时数据、环境变量等),它作为参数被显式地传递进来。函数内部不引用任何外部或上一轮调用残留的变量,它只基于传入的参数进行计算,并返回全新的状态和输出。

这种设计的优势立刻显现:

  • 确定性 :相同的 (state, input) 输入,永远产生相同的 (new_state, output) 输出。
  • 易于测试 :你只需要构造输入参数,调用函数,断言输出即可。无需关心对象初始化。
  • 易于组合 :因为代理就是函数,你可以用高阶函数(如 pipe , compose )轻松地将多个代理串联或并联起来,形成更复杂的工作流。每个代理都只关心自己的输入输出,互不干扰。
  • 易于持久化与恢复 :整个系统的状态就是那个 state 字典或对象,你可以随时将其序列化(如保存为JSON)到数据库或文件中,也可以随时反序列化回来,精确恢复到某一刻的执行现场。

2.2 项目架构与核心抽象

rerecoy123/statelessagent 的实现通常围绕几个核心抽象来构建。虽然具体实现可能因版本而异,但其思想是相通的。

1. Agent(代理) 这是最核心的抽象。它通常被定义为一个可调用对象。一个最简单的 Agent 可能就是一个Python函数。但项目通常会提供一个基础类或装饰器,来统一接口并附加一些能力(如日志、错误处理)。

# 一个最简单的Agent函数示例
def greeting_agent(context, name):
    """一个简单的问候代理"""
    history = context.get('history', [])
    new_message = f"Hello, {name}!"
    history.append({'role': 'assistant', 'content': new_message})
    # 返回新的上下文
    return {
        **context,
        'history': history,
        'last_response': new_message
    }

# 使用
current_context = {'history': []}
new_context = greeting_agent(current_context, 'Alice')
print(new_context['last_response']) # 输出: Hello, Alice!

2. Context(上下文) 这是贯穿整个工作流的“状态载体”。它通常是一个字典(或类似字典的对象),包含了当前任务所需的所有信息。常见的键包括:

  • input : 本次调用的原始输入。
  • history : 对话或交互历史记录。
  • memory : 从历史或外部知识库提取的长期记忆。
  • intermediate_results : 多步流程中,每一步产生的中间结果。
  • environment : 环境变量或配置。 上下文在代理之间流动,每个代理读取其中一部分,并可能添加或修改一部分,然后传递给下一个代理。

3. Workflow / Pipeline(工作流) 这是将多个无状态代理组合起来的关键。项目会提供一些工具来创建顺序、并行或条件分支的工作流。

# 伪代码示例:一个简单的工作流组合
from some_module import pipe, conditional

workflow = pipe(
    agent_preprocess,   # 代理A:预处理输入
    agent_think,        # 代理B:思考决策
    conditional(        # 条件分支:根据某个结果选择不同路径
        check=agent_decide_route,
        then=agent_handle_complex,
        else_=agent_handle_simple
    ),
    agent_format_output # 代理C:格式化输出
)

# 执行工作流
final_context = workflow(initial_context, user_input)

这种声明式的组合方式,使得复杂的业务逻辑变得清晰可见。

2.3 与常见框架的对比思考

你可能会想,这和 LangChain Chain AutoGen Agent 有什么区别?核心区别在于 状态管理的哲学

  • LangChain :其 Chain 虽然也强调组合,但很多组件(如 Memory )是有状态的,并且状态管理往往与组件绑定,链的执行过程会隐式地更新这些状态。这提供了便利,但牺牲了一些透明度和可控性。
  • AutoGen :其 ConversableAgent 是典型的有状态代理,内部维护着对话历史。多代理协作时,状态在代理间通过消息传递,但每个代理内部的状态是封装的。

statelessagent 更像是一种 底层模式 设计原则 。它不试图提供一个“开箱即用”的全功能对话代理,而是提供了一套构建块,让你可以基于纯函数和显式状态流来构建任何你想要的代理系统。它更轻量,更符合函数式编程的思想,让你对数据流有百分百的控制权。你可以用它来实现一个简化版的 LangChain Chain ,或者一个更可控的 AutoGen 式多代理系统。

注意 :选择 statelessagent 意味着你需要自己管理更多的细节(比如状态的结构设计、历史记录的持久化策略),但换来的则是极致的灵活性和可调试性。如果你的项目对可控性、可测试性要求极高,或者你希望核心逻辑不依赖于某个重型框架,那么这是一个绝佳的选择。

3. 从零开始实现一个无状态代理工作流

理论说得再多,不如动手实践。让我们抛开复杂的框架,就用最朴素的Python,来实现一个基于无状态代理思想的小型问答系统。这个系统将包含:输入解析、知识检索、推理生成三个步骤。

3.1 定义上下文与代理函数

首先,我们定义我们的上下文结构。我们将使用一个简单的字典。

from typing import Dict, List, Any, Callable

# 定义Context类型别名,方便注解
Context = Dict[str, Any]

# 初始化一个上下文
initial_context: Context = {
    "user_input": "",
    "history": [], # 存放交互历史,每一项可能是 {"role": "user"/"assistant", "content": "..."}
    "retrieved_knowledge": [],
    "reasoning_chain": [],
    "final_answer": None,
    "error": None
}

接下来,我们实现第一个代理: 输入解析代理 。它的职责是清理和标准化用户输入。

def input_parser_agent(context: Context, raw_input: str) -> Context:
    """
    无状态代理:输入解析。
    参数:
        context: 当前上下文。
        raw_input: 原始用户输入字符串。
    返回:
        更新后的上下文,包含解析后的输入。
    """
    print(f"[Input Parser] 收到原始输入: {raw_input}")
    # 1. 去除首尾空白
    cleaned_input = raw_input.strip()
    # 2. 简单的情感词检测(示例)
    positive_words = ["谢谢", "很好", "不错"]
    negative_words = ["糟糕", "错误", "不对"]
    sentiment = "neutral"
    for word in positive_words:
        if word in cleaned_input:
            sentiment = "positive"
            break
    for word in negative_words:
        if word in cleaned_input:
            sentiment = "negative"
            break

    # 3. 更新上下文
    new_history = context.get("history", []) + [{"role": "user", "content": cleaned_input}]
    new_context = {
        **context, # 使用解包语法复制原上下文
        "user_input": cleaned_input,
        "history": new_history,
        "parsed_sentiment": sentiment,
        "step_log": context.get("step_log", []) + ["Input parsed successfully"]
    }
    print(f"[Input Parser] 解析完成。情感: {sentiment}")
    return new_context

3.2 构建知识检索与推理代理

假设我们有一个简单的“知识库”,这里用字典模拟。

# 模拟一个知识库
SIMULATED_KNOWLEDGE_BASE = {
    "python": "Python是一种高级、解释型的通用编程语言,以简洁易读著称。",
    "无状态": "在计算机科学中,无状态是指系统或组件不保存客户端请求之间的状态信息,每次请求都包含处理所需的所有信息。",
    "代理": "代理(Agent)是一种能够感知环境并自主行动以实现目标的计算机程序或实体。",
}

def knowledge_retrieval_agent(context: Context) -> Context:
    """
    无状态代理:知识检索。
    从知识库中查找与用户输入相关的信息。
    """
    user_input = context.get("user_input", "")
    print(f"[Knowledge Retrieval] 正在检索关键词: {user_input}")
    retrieved = []
    # 简单的关键词匹配(实际应用会用Embedding和向量数据库)
    for keyword, info in SIMULATED_KNOWLEDGE_BASE.items():
        if keyword in user_input.lower():
            retrieved.append({"keyword": keyword, "content": info})

    new_context = {
        **context,
        "retrieved_knowledge": retrieved,
        "step_log": context.get("step_log", []) + [f"Retrieved {len(retrieved)} knowledge snippets"]
    }
    print(f"[Knowledge Retrieval] 检索到 {len(retrieved)} 条信息。")
    return new_context

现在实现 推理生成代理 ,它综合用户输入和检索到的知识生成回答。

def reasoning_agent(context: Context) -> Context:
    """
    无状态代理:推理与回答生成。
    基于输入和检索到的知识生成回答。
    """
    user_input = context.get("user_input", "")
    knowledge = context.get("retrieved_knowledge", [])
    history = context.get("history", [])

    reasoning_steps = []
    answer = ""

    # 推理步骤1:检查是否有相关知识
    if knowledge:
        reasoning_steps.append("在知识库中找到了相关信息。")
        # 简单拼接知识作为回答基础
        knowledge_text = " ".join([item['content'] for item in knowledge])
        answer = f"根据相关信息:{knowledge_text}\n对于你的问题“{user_input}”,我的理解如上。"
    else:
        reasoning_steps.append("未在知识库中找到直接相关信息。")
        # 根据情感做出不同回应
        sentiment = context.get("parsed_sentiment", "neutral")
        if sentiment == "positive":
            answer = f“感谢你的积极反馈!关于‘{user_input}’,我目前的知识库中没有专门信息,但我会继续学习。”
        elif sentiment == "negative":
            answer = f“抱歉让你感到不满意。关于‘{user_input}’,我暂时无法提供准确答案,我会努力改进。”
        else:
            answer = f“我理解你想了解‘{user_input}’,但我目前的知识库中还没有这方面的详细资料。”

    # 更新历史
    new_history = history + [{"role": "assistant", "content": answer}]

    new_context = {
        **context,
        "reasoning_chain": reasoning_steps,
        "final_answer": answer,
        "history": new_history,
        "step_log": context.get("step_log", []) + ["Reasoning and answer generation completed"]
    }
    print(f"[Reasoning Agent] 推理完成。回答长度: {len(answer)}")
    return new_context

3.3 组合代理与执行工作流

有了独立的代理函数,我们可以轻松地将它们组合成一个完整的工作流。这里我们手动实现一个简单的 pipe 函数。

def pipe(*agents):
    """
    一个简单的高阶函数,用于将多个无状态代理按顺序组合。
    每个代理的签名应为 (context) -> new_context。
    第一个代理接受初始context和可能的额外参数,后续代理只接受context。
    这里我们做简化处理。
    """
    def workflow(initial_context, *args, **kwargs):
        context = initial_context
        # 处理第一个可能接受额外参数的代理(如input_parser_agent)
        if agents:
            # 我们约定第一个代理可以接收额外参数
            context = agents[0](context, *args, **kwargs)
            # 后续代理只接收context
            for agent in agents[1:]:
                context = agent(context)
        return context
    return workflow

# 组合我们的工作流
qa_workflow = pipe(input_parser_agent, knowledge_retrieval_agent, reasoning_agent)

# 执行工作流
print("=== 开始执行无状态代理工作流 ===")
final_context = qa_workflow(initial_context, "请解释一下什么是Python?")
print("\n=== 工作流执行结果 ===")
print("最终回答:", final_context.get("final_answer"))
print("\n=== 完整上下文快照 ===")
import pprint
pprint.pprint({k: v for k, v in final_context.items() if k != 'history'}) # 简略打印
print("历史记录长度:", len(final_context.get('history', [])))

运行上述代码,你会看到一个清晰的、分步骤的执行过程。每个代理都像一个独立的、可测试的零件,它们通过上下文字典进行通信,没有任何隐藏的副作用。

实操心得 :在定义上下文结构时,建议提前规划好键名并形成文档。一个混乱的上下文字典会让工作流难以维护。可以考虑使用 TypedDict Pydantic 模型来赋予上下文结构更强的类型约束和自文档化能力,这在团队协作中尤为重要。

4. 高级模式与工程化实践

基础的工作流跑通了,但要用于实际项目,我们还需要考虑更多工程化问题。

4.1 错误处理与上下文恢复

无状态代理的一个巨大优势是错误恢复。因为状态是显式的,当某个代理失败时,我们可以捕获错误,将错误信息存入上下文,然后由后续的“错误处理代理”决定是重试、降级处理还是终止流程。

def safe_agent(agent_func):
    """一个装饰器,用于包装代理,提供错误处理。"""
    def wrapper(context: Context, *args, **kwargs):
        try:
            return agent_func(context, *args, **kwargs)
        except Exception as e:
            print(f"[Safe Agent] 代理 {agent_func.__name__} 执行出错: {e}")
            # 将错误信息记录到上下文中,而不是直接抛出
            error_context = {
                **context,
                "error": {
                    "agent": agent_func.__name__,
                    "exception": str(e),
                    "timestamp": "2023-10-27T10:00:00" # 应使用实际时间
                },
                "step_log": context.get("step_log", []) + [f"Agent {agent_func.__name__} failed: {e}"]
            }
            # 可以在这里触发一个错误处理工作流
            # error_handling_workflow(error_context)
            return error_context # 返回包含错误信息的上下文,让下游代理决定如何处置
    return wrapper

# 用装饰器包装代理
@safe_agent
def potentially_failing_agent(context):
    # 模拟一个可能失败的代理
    if "fail" in context.get("user_input", ""):
        raise ValueError("Simulated failure triggered by user input.")
    return {**context, "result": "success"}

# 测试
ctx = {"user_input": "please fail"}
result = potentially_failing_agent(ctx)
print("出错后的上下文:", result.get("error"))

4.2 异步支持与性能考量

对于需要调用外部API(如LLM)或进行I/O操作的代理,异步是必须的。我们可以定义异步的代理函数。

import asyncio

async def async_llm_agent(context: Context) -> Context:
    """模拟调用异步LLM API的代理。"""
    prompt = context.get("user_input", "")
    print(f"[Async LLM Agent] 正在异步处理提示: {prompt[:50]}...")
    # 模拟网络延迟
    await asyncio.sleep(0.5)
    # 模拟LLM响应
    llm_response = f"模拟LLM对‘{prompt}’的思考结果。"
    return {**context, "llm_response": llm_response}

async def async_workflow(initial_context, user_input):
    """异步工作流示例。"""
    ctx = input_parser_agent(initial_context, user_input)
    ctx = knowledge_retrieval_agent(ctx)
    # 执行异步代理
    ctx = await async_llm_agent(ctx)
    ctx = reasoning_agent(ctx) # reasoning_agent可以是同步的
    return ctx

# 运行异步工作流
# final_ctx = asyncio.run(async_workflow(initial_context, "异步问题"))

在实际项目中,你可能需要一个异步的 pipe 函数来组合同步和异步代理。

4.3 测试策略:无状态带来的红利

无状态代理的测试极其简单。因为每个代理都是纯函数,你只需要准备输入上下文,调用函数,断言输出上下文。

import pytest

def test_input_parser_agent():
    # 准备测试输入
    initial_ctx = {"history": [], "step_log": []}
    raw_input = " 你好,世界! "
    # 执行被测代理
    result_ctx = input_parser_agent(initial_ctx, raw_input)
    # 断言结果
    assert result_ctx["user_input"] == "你好,世界!"
    assert result_ctx["parsed_sentiment"] == "neutral"
    assert len(result_ctx["history"]) == 1
    assert result_ctx["history"][0]["content"] == "你好,世界!"
    assert "Input parsed successfully" in result_ctx["step_log"]
    print("测试通过!")

def test_knowledge_retrieval_agent_found():
    ctx = {"user_input": "告诉我python的特点"}
    result = knowledge_retrieval_agent(ctx)
    assert len(result["retrieved_knowledge"]) > 0
    assert any(item['keyword'] == 'python' for item in result['retrieved_knowledge'])

def test_knowledge_retrieval_agent_not_found():
    ctx = {"user_input": "无关内容"}
    result = knowledge_retrieval_agent(ctx)
    assert len(result["retrieved_knowledge"]) == 0

if __name__ == "__main__":
    test_input_parser_agent()
    test_knowledge_retrieval_agent_found()
    test_knowledge_retrieval_agent_not_found()

你可以轻松地为每个代理编写单元测试,并为整个工作流编写集成测试(只需测试组合后的函数)。Mock外部依赖(如知识库、LLM API)也变得非常直接。

4.4 与现有生态集成

无状态代理模式并不排斥使用其他强大的库。恰恰相反,它可以很好地集成它们。

  • 集成LLM调用 :你可以创建一个 call_llm_agent ,内部使用 openai 库或 litellm ,但它仍然保持无状态接口 (context) -> new_context
  • 集成向量数据库 :你的 knowledge_retrieval_agent 内部可以使用 chromadb pinecone 的客户端进行查询。
  • 集成外部工具 :可以创建代理来调用搜索引擎API、计算器、代码执行器等,所有调用结果都更新到上下文中。

关键在于,这些复杂的依赖被封装在一个个无状态的“黑盒”函数里,对外只暴露清晰的输入输出接口。这极大地降低了系统的耦合度。

5. 常见问题、排查技巧与实战建议

在实际应用无状态代理模式时,你会遇到一些典型问题。以下是我踩过坑后总结的经验。

5.1 上下文膨胀与性能问题

问题:随着工作流执行步骤增多,上下文字典可能变得非常庞大(尤其是存储了长历史或大量中间结果),导致内存占用高和序列化/反序列化速度慢。

解决方案与技巧:

  1. 选择性存储 :并非所有数据都需要一直保留在上下文里。可以设计一个“上下文修剪”代理,在流程的关键节点后,移除不再需要的中间数据。
    def context_pruner_agent(context: Context, keys_to_keep: List[str]) -> Context:
        """修剪上下文,只保留指定的键。"""
        pruned_context = {k: context[k] for k in keys_to_keep if k in context}
        # 或者,更常见的做法是复制整个上下文,但清空某些大型字段
        new_context = {**context}
        if 'large_intermediate_data' in new_context:
            new_context['large_intermediate_data'] = None # 或直接删除 del new_context[...]
        return new_context
    
  2. 分治策略 :对于超长的工作流,可以将其分解为多个子工作流。每个子工作流有自己独立的、较小的上下文。子工作流之间通过定义良好的接口(如一个精简的结果摘要)进行通信。
  3. 使用高效的数据结构 :对于列表形式的历史记录,如果条目极多,可以考虑使用 collections.deque 并设置最大长度。对于复杂的嵌套结构,可以考虑使用 orjson 替代标准 json 库进行序列化,速度更快。

5.2 代理间通信与接口约定混乱

问题:当团队多人开发多个代理时,如果没有清晰的约定,代理之间可能因为期望的上下文键名或数据结构不同而无法协作。

解决方案与技巧:

  1. 定义并共享上下文模式(Schema) :强烈建议使用 Pydantic BaseModel 来定义你的上下文。这提供了类型检查、自动验证和文档。
    from pydantic import BaseModel, Field
    from typing import Optional, List
    
    class ConversationHistory(BaseModel):
        role: str
        content: str
    
    class AgentContext(BaseModel):
        user_input: str
        history: List[ConversationHistory] = Field(default_factory=list)
        retrieved_knowledge: Optional[List[dict]] = None
        final_answer: Optional[str] = None
        error: Optional[dict] = None
    
        class Config:
            extra = "allow" # 允许额外的键,保持灵活性
    
    # 在代理中使用
    def validated_agent(context_dict: dict) -> dict:
        # 1. 验证输入上下文是否符合模式
        ctx_obj = AgentContext(**context_dict)
        # 2. 执行业务逻辑...
        ctx_obj.final_answer = "Processed"
        # 3. 返回字典(或直接返回模型对象)
        return ctx_obj.dict()
    
  2. 建立代理“契约”文档 :为每个代理编写简短的文档,说明它期望从上下文中读取哪些字段(输入),以及它会写入或修改哪些字段(输出)。
  3. 使用版本控制 :如果上下文模式需要变更,考虑引入版本字段,并编写“上下文迁移”代理来处理不同版本上下文的兼容性问题。

5.3 调试与日志记录困难

问题:当工作流复杂时,如果某个环节结果不对,很难追踪是哪个代理出了问题,以及当时的具体状态是什么。

解决方案与技巧:

  1. 结构化日志集成到上下文 :就像我们示例中的 step_log 字段,每个代理都将关键操作和决策以结构化的方式记录到上下文中。最终,整个工作流的执行轨迹一目了然。
  2. 设计一个“调试开关” :在上下文中设置一个 debug: bool 字段。当它为 True 时,每个代理可以打印更详细的内部信息;为 False 时,则保持静默。
    def detailed_agent(context: Context):
        if context.get("debug"):
            print(f"[DEBUG] {详细内部状态}")
        # ... 正常逻辑
        return new_context
    
  3. 可视化工作流执行 :可以开发一个简单的工具,将工作流的定义(代理列表)和一次执行的上下文日志( step_log )结合起来,生成一个可视化的执行流程图,直观显示数据流和每个节点的输入输出快照。

5.4 何时选择无状态代理模式?

无状态代理模式并非银弹,它有最适合的场景:

  • ✅ 强烈推荐 :需要对数据流有绝对控制权和可见性的项目;需要极高可测试性的关键业务逻辑;希望将AI能力(LLM调用)与业务逻辑清晰解耦的系统;构建可解释性强的自动化流程。
  • ⚠️ 需要权衡 :超简单的、一次性的脚本(可能杀鸡用牛刀);极度追求开发速度、愿意接受框架黑盒的快速原型阶段。
  • ❌ 可能不适用 :对极致性能有要求,无法接受每次传递完整上下文带来的开销(尽管通常这开销很小);项目严重依赖某个现有有状态框架(如LangChain)的特定高级特性,且重写成本过高。

我的个人体会是 ,无状态代理更像是一种 底层架构思想 。你可以从一个小模块开始尝试,比如用无状态代理重构你系统中一个负责“信息提取”的复杂函数。体会其带来的测试便利和清晰的数据流后,再决定是否在更大范围推广。它不一定替代像LangChain这样的高层框架,但可以让你在框架之下,构建出更健壮、更可控的核心业务模块。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐