Agent心智架构:感知一推理一行动循环

Agent 并非一个“函数”,而是一个“进程”——运行在一个while(alive)的无限循环之中。在这个循环中,感知、推理与行动并非孤立的步骤,而是彼此咬合、协同运转的“齿轮”。基于对 Agent 本体论的认知,我们可以构建出 Agent 心智架构,如图 3-7 所示。
在这里插入图片描述
代码如下:

#!/usr/bin/env python3
"""
本示例展示了如何实现一个带有工具调用能力的 AI Agent 循环。
Agent 循环的核心逻辑保持不变,只是在工具数组中添加了工具定义,
并通过一个分发映射表(dispatch map)来路由工具调用请求。

数据流向图:

    +----------+      +-------+      +------------------+
    |   User   | ---> |  LLM  | ---> | Tool Dispatch    |
    |  prompt  |      |       |      | {                |
    +----------+      +---+---+      |   bash: run_bash |
                          ^          |   read: run_read |
                          |          |   write: run_wr  |
                          +----------+   edit: run_edit |
                          tool_result| }                |
                                     +------------------+
"""

# =============================================================================
# 标准库导入
# =============================================================================
import os          # 操作系统接口,用于环境变量和路径操作
import subprocess  # 子进程管理,用于执行 shell 命令
import json        # JSON 解析,用于解析工具调用的参数
from pathlib import Path  # 面向对象的路径操作
import logging

# 配置日志格式和级别
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# =============================================================================
# 第三方库导入
# =============================================================================
from openai import OpenAI    # OpenAI 官方 Python SDK
from dotenv import load_dotenv  # 从 .env 文件加载环境变量

# =============================================================================
# 环境配置
# =============================================================================

# 加载 .env 文件中的环境变量
# override=True 表示即使系统已有同名环境变量,也用 .env 文件中的值覆盖
load_dotenv(override=True)

# 工作目录:当前脚本运行的位置
# 所有文件操作都会相对于这个目录进行
WORKDIR = Path.cwd()
# 初始化 OpenAI 客户端
# 注意:虽然变量名叫 ANTHROPIC_API_KEY,但这里实际是 DashScope 的 API Key
# 这种命名是为了方便切换不同的后端服务
client = OpenAI(
    api_key=os.getenv("ANTHROPIC_API_KEY"),  # 从环境变量获取 API 密钥
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",  # DashScope 兼容接口地址
)

# 模型配置:从环境变量获取,默认使用 qwen-plus
# 可选值包括:qwen-turbo, qwen-plus, qwen-max 等
MODEL = os.getenv("MODEL_ID", "qwen-plus")

# System Prompt: 定义 Agent 的角色和行为准则
# 这里告诉模型它是一个编码代理,应该使用工具来完成任务,并且行动优于解释
SYSTEM = f"You are a coding agent at {WORKDIR}. Use tools to solve tasks. Act, don't explain."


# =============================================================================
# 安全工具函数
# =============================================================================

def safe_path(p: str) -> Path:
    """
    安全路径解析函数 - 防止路径遍历攻击
    
    将用户提供的相对路径转换为绝对路径,并验证该路径是否在允许的工作目录内。
    这可以防止恶意用户尝试访问工作目录之外的敏感文件。
    
    参数:
        p: 用户提供的文件路径(相对路径)
    
    返回:
        Path: 解析后的安全绝对路径对象
    
    异常:
        ValueError: 如果路径试图逃逸工作目录
    
    示例:
        >>> safe_path("src/main.py")  # 正常路径
        Path("/home/user/project/src/main.py")
        
        >>> safe_path("../../../etc/passwd")  # 恶意路径
        ValueError: Path escapes workspace: ../../../etc/passwd
    """
    # 将相对路径解析为绝对路径
    # WORKDIR / p 会将路径拼接到工作目录
    # .resolve() 会解析所有符号链接和相对路径组件(如 ..)
    path = (WORKDIR / p).resolve()
    
    # 安全检查:确保解析后的路径仍然是工作目录的子路径
    # is_relative_to() 方法检查路径是否在指定目录内
    if not path.is_relative_to(WORKDIR):
        raise ValueError(f"Path escapes workspace: {p}")
    
    return path


# =============================================================================
# 工具执行函数
# =============================================================================

def run_bash(command: str) -> str:
    """
    Bash 命令执行工具
    
    在 shell 中执行指定的命令,并返回输出结果。
    包含安全检查,阻止危险命令的执行。
    
    参数:
        command: 要执行的 shell 命令字符串
    
    返回:
        str: 命令的标准输出和标准错误输出(合并),最多返回 50000 字符
    
    安全措施:
        1. 阻止危险命令:rm -rf /, sudo, shutdown, reboot, > /dev/
        2. 设置超时限制:120 秒
        3. 输出截断:防止返回过多数据
    
    示例:
        >>> run_bash("ls -la")
        "total 48\ndrwxr-xr-x 2 user user 4096 ..."
        
        >>> run_bash("sudo rm -rf /")
        "Error: Dangerous command blocked"
    """
    # 危险命令黑名单
    # 这些命令可能对系统造成严重损害,因此被完全阻止
    dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
    
    # 检查命令是否包含任何危险关键词
    if any(d in command for d in dangerous):
        return "Error: Dangerous command blocked"
    
    try:
        # 执行 shell 命令
        # shell=True: 通过 shell 执行命令(支持管道、通配符等 shell 特性)
        # cwd=WORKDIR: 在工作目录中执行命令
        # capture_output=True: 捕获 stdout 和 stderr
        # text=True: 将输出解码为字符串而非字节
        # timeout=120: 设置 120 秒超时限制
        r = subprocess.run(
            command, 
            shell=True, 
            cwd=WORKDIR,
            capture_output=True, 
            text=True, 
            timeout=120
        )
        
        # 合并标准输出和标准错误输出
        # .strip() 移除首尾空白字符
        out = (r.stdout + r.stderr).strip()
        
        # 截断输出,防止返回过多数据
        # 如果输出为空,返回提示信息
        return out[:50000] if out else "(no output)"
        
    except subprocess.TimeoutExpired:
        # 命令执行超时
        return "Error: Timeout (120s)"


def run_read(path: str, limit: int = None) -> str:
    """
    文件读取工具
    
    读取指定文件的内容,支持限制读取行数。
    包含路径安全检查,防止读取工作目录外的文件。
    
    参数:
        path: 文件路径(相对于工作目录)
        limit: 可选参数,限制读取的行数。如果文件行数超过限制,
               会在末尾添加省略提示
    
    返回:
        str: 文件内容,最多返回 50000 字符
    
    示例:
        >>> run_read("README.md")
        "# Project Name\nThis is a readme file..."
        
        >>> run_read("large_file.txt", limit=10)
        "Line 1\nLine 2\n... Line 10\n... (990 more lines)"
    """
    try:
        # 使用 safe_path 进行安全路径解析
        # .read_text() 读取整个文件内容为字符串
        text = safe_path(path).read_text()
        
        # 按行分割文本
        lines = text.splitlines()
        
        # 如果设置了行数限制且文件行数超过限制
        if limit and limit < len(lines):
            # 截取指定行数
            lines = lines[:limit]
            # 添加省略提示,显示剩余行数
            lines = lines + [f"... ({len(lines) - limit} more lines)"]
        
        # 重新拼接为字符串,并截断到 50000 字符
        return "\n".join(lines)[:50000]
        
    except Exception as e:
        # 捕获并返回任何错误(如文件不存在、权限不足等)
        return f"Error: {e}"


def run_write(path: str, content: str) -> str:
    """
    文件写入工具
    
    将内容写入指定文件。如果文件不存在会创建新文件,
    如果文件所在目录不存在也会自动创建目录。
    
    参数:
        path: 文件路径(相对于工作目录)
        content: 要写入的文件内容
    
    返回:
        str: 操作结果信息,包含写入的字节数和文件路径
    
    安全措施:
        1. 使用 safe_path 防止路径遍历攻击
        2. 自动创建父目录
    
    示例:
        >>> run_write("output.txt", "Hello, World!")
        "Wrote 13 bytes to output.txt"
    """
    try:
        # 安全路径解析
        fp = safe_path(path)
        
        # 创建父目录(如果不存在)
        # parents=True: 创建所有必要的父目录
        # exist_ok=True: 如果目录已存在不报错
        fp.parent.mkdir(parents=True, exist_ok=True)
        
        # 写入文件内容
        fp.write_text(content)
        
        # 返回操作结果
        return f"Wrote {len(content)} bytes to {path}"
        
    except Exception as e:
        return f"Error: {e}"


def run_edit(path: str, old_text: str, new_text: str) -> str:
    """
    文件编辑工具
    
    在文件中查找并替换精确匹配的文本。只会替换第一个匹配项。
    
    参数:
        path: 文件路径(相对于工作目录)
        old_text: 要查找的原始文本(必须精确匹配)
        new_text: 替换后的新文本
    
    返回:
        str: 操作结果信息
    
    注意:
        - 使用精确字符串匹配,区分大小写
        - 只替换第一个匹配项,不会替换所有出现
    
    示例:
        >>> run_edit("config.py", "DEBUG = False", "DEBUG = True")
        "Edited config.py"
        
        >>> run_edit("config.py", "not_exist", "new_value")
        "Error: Text not found in config.py"
    """
    try:
        # 安全路径解析
        fp = safe_path(path)
        
        # 读取当前文件内容
        content = fp.read_text()
        
        # 检查要替换的文本是否存在
        if old_text not in content:
            return f"Error: Text not found in {path}"
        
        # 执行替换操作
        # .replace(old, new, 1) 中的 1 表示只替换第一个匹配项
        fp.write_text(content.replace(old_text, new_text, 1))
        
        return f"Edited {path}"
        
    except Exception as e:
        return f"Error: {e}"


# =============================================================================
# 工具分发映射表
# =============================================================================

# 工具名称到处理函数的映射
# 当 LLM 调用某个工具时,通过这个字典找到对应的处理函数
# 使用 lambda 函数来统一参数传递方式(从字典中提取参数)
#
# 工作原理:
# 1. LLM 返回工具调用请求,包含工具名称和参数
# 2. 通过 TOOL_HANDLERS[tool_name] 获取对应的 lambda 函数
# 3. 调用 lambda 函数,传入参数字典 **kwargs
# 4. lambda 函数将参数分发给实际的执行函数
TOOL_HANDLERS = {
    "bash":       lambda **kw: run_bash(kw["command"]),
    "read_file":  lambda **kw: run_read(kw["path"], kw.get("limit")),
    "write_file": lambda **kw: run_write(kw["path"], kw["content"]),
    "edit_file":  lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
}


# =============================================================================
# 工具定义 (OpenAI 格式)
# =============================================================================

# OpenAI 工具定义格式说明:
# - 每个工具是一个字典,包含 "type" 和 "function" 两个键
# - "type" 目前只支持 "function"
# - "function" 包含:
#   - name: 工具名称(必须与 TOOL_HANDLERS 中的键一致)
#   - description: 工具描述(帮助 LLM 理解工具用途)
#   - parameters: JSON Schema 格式的参数定义
TOOLS = [
    # -------------------------------------------------------------------------
    # bash 工具: 执行 shell 命令
    # -------------------------------------------------------------------------
    {
        "type": "function",  # 工具类型,OpenAI 目前只支持 "function"
        "function": {
            "name": "bash",  # 工具名称,必须与 TOOL_HANDLERS 中的键匹配
            "description": "Run a shell command.",  # 工具描述,LLM 会根据这个决定是否使用
            "parameters": {  # 参数定义,使用 JSON Schema 格式
                "type": "object",  # 参数必须是一个对象(字典)
                "properties": {
                    "command": {
                        "type": "string",  # 参数类型为字符串
                        "description": "The shell command to execute"  # 参数描述
                    }
                },
                "required": ["command"]  # 必需参数列表
            }
        }
    },
    
    # -------------------------------------------------------------------------
    # read_file 工具: 读取文件内容
    # -------------------------------------------------------------------------
    {
        "type": "function",
        "function": {
            "name": "read_file",
            "description": "Read file contents.",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {
                        "type": "string",
                        "description": "The file path to read"
                    },
                    "limit": {
                        "type": "integer",  # 整数类型
                        "description": "Maximum number of lines to read"
                        # 注意: limit 不在 required 中,是可选参数
                    }
                },
                "required": ["path"]  # 只有 path 是必需的
            }
        }
    },
    
    # -------------------------------------------------------------------------
    # write_file 工具: 写入文件
    # -------------------------------------------------------------------------
    {
        "type": "function",
        "function": {
            "name": "write_file",
            "description": "Write content to file.",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {
                        "type": "string",
                        "description": "The file path to write"
                    },
                    "content": {
                        "type": "string",
                        "description": "The content to write"
                    }
                },
                "required": ["path", "content"]  # 两个参数都是必需的
            }
        }
    },
    
    # -------------------------------------------------------------------------
    # edit_file 工具: 编辑文件(查找替换)
    # -------------------------------------------------------------------------
    {
        "type": "function",
        "function": {
            "name": "edit_file",
            "description": "Replace exact text in file.",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {
                        "type": "string",
                        "description": "The file path to edit"
                    },
                    "old_text": {
                        "type": "string",
                        "description": "The text to replace"
                    },
                    "new_text": {
                        "type": "string",
                        "description": "The new text"
                    }
                },
                "required": ["path", "old_text", "new_text"]  # 三个参数都是必需的
            }
        }
    },
]


# =============================================================================
# Agent 循环核心函数
# =============================================================================

def agent_loop(messages: list):
    """
    Agent 主循环函数
    
    这是整个系统的核心,实现了 LLM 与工具之间的交互循环:
    1. 将对话历史发送给 LLM
    2. 检查 LLM 是否需要调用工具
    3. 如果需要,执行工具并将结果返回给 LLM
    4. 重复直到 LLM 返回最终回复
    
    参数:
        messages: 对话历史列表,包含 user 和 assistant 的消息
    
    工作流程:
        ┌─────────────────────────────────────┐
        │  发送消息给 LLM (包含工具定义)        │
        └─────────────────┬───────────────────┘
                          ▼
        ┌─────────────────────────────────────┐
        │  LLM 返回响应                        │
        └─────────────────┬───────────────────┘
                          ▼
                ┌─────────────────┐
                │  有工具调用?     │
                └────────┬────────┘
                    ┌────┴────┐
                   是         否
                    │          │
                    ▼          ▼
        ┌──────────────┐  ┌──────────────┐
        │ 执行所有工具  │  │ 返回最终回复  │
        │ 返回结果给LLM │  │              │
        └──────┬───────┘  └──────────────┘
               │
               └──────► 返回步骤 1 (循环)
    
    OpenAI 特有细节:
        - tool_choice="auto": 让模型自动决定是否使用工具
        - tool_calls: 包含所有工具调用的列表
        - tool_call_id: 每个工具调用都有唯一 ID,结果需要关联
    """
    while True:
        # =====================================================================
        # 步骤 1: 调用 OpenAI API
        # =====================================================================
        response = client.chat.completions.create(
            model=MODEL,  # 使用的模型
            # 构建消息列表: system prompt + 对话历史
            # 注意: OpenAI 将 system 作为 messages 列表的一部分
            messages=[{"role": "system", "content": SYSTEM}] + messages,
            tools=TOOLS,  # 工具定义列表
            tool_choice="auto",  # 让模型自动决定是否使用工具
            max_tokens=8000,  # 最大生成 token 数
        )
        
        # =====================================================================
        # 步骤 2: 获取助手消息并添加到历史
        # =====================================================================
        # response.choices[0].message 是助手的回复消息对象
        assistant_message = response.choices[0].message
        
        
        # ⚠️ 重要: 必须将 Pydantic 模型对象转换为字典格式
        # 如果直接添加 assistant_message 对象,当包含 tool_calls 时,
        # 下次 API 调用会报错: TypeError: argument 'by_alias': 'NoneType' object
        # 使用 model_dump(exclude_none=True) 序列化为字典
        messages.append(assistant_message.model_dump(exclude_none=True))
        logger.info(f"当前回应大模型的数据消息: {assistant_message.model_dump(exclude_none=True)}")
        
        # =====================================================================
        # 步骤 3: 检查是否需要调用工具
        # =====================================================================
        # tool_calls 属性为 None 或空列表表示模型不需要调用工具
        # 这意味着模型已经生成了最终回复
        if not assistant_message.tool_calls:
            # 没有工具调用,打印最终回复并返回
            if assistant_message.content:
                print(assistant_message.content)
            return  # 退出循环,返回到调用者
        
        # =====================================================================
        # 步骤 4: 处理所有工具调用
        # =====================================================================
        tool_results = []  # 存储所有工具执行结果
        
        # 遍历每个工具调用请求
        # 注意: 模型可能一次请求调用多个工具(并行调用)
        for tool_call in assistant_message.tool_calls:
            # -----------------------------------------------------------------
            # 提取工具信息
            # -----------------------------------------------------------------
            # tool_call.function.name: 工具名称
            tool_name = tool_call.function.name
            
            # tool_call.function.arguments: 工具参数(JSON 字符串)
            # 需要用 json.loads() 解析为 Python 字典
            tool_args = json.loads(tool_call.function.arguments)
            
            # -----------------------------------------------------------------
            # 执行工具
            # -----------------------------------------------------------------
            # 从分发映射表中获取对应的处理函数
            handler = TOOL_HANDLERS.get(tool_name)
            logger.info(f"工具调用请求: {tool_name} with args {tool_args}")
            
            if handler:
                # 调用处理函数,传入参数
                # **tool_args 将字典解包为关键字参数
                output = handler(**tool_args)
            else:
                # 未知工具,返回错误信息
                output = f"Unknown tool: {tool_name}"
            
            # 打印工具执行结果(截取前 200 字符)
            print(f"> {tool_name}: {output[:200]}")
            
            # -----------------------------------------------------------------
            # 构建工具结果消息
            # -----------------------------------------------------------------
            # OpenAI 要求工具结果包含以下字段:
            # - tool_call_id: 关联回原始工具调用的唯一 ID
            # - role: 必须是 "tool"
            # - name: 工具名称(可选但推荐)
            # - content: 工具执行的输出结果
            tool_results.append({
                "tool_call_id": tool_call.id,  # 关联 ID,必须与 tool_call.id 匹配
                "role": "tool",  # 固定为 "tool"
                "name": tool_name,  # 工具名称
                "content": output  # 工具输出
            })
        
        # =====================================================================
        # 步骤 5: 将工具结果添加到对话历史
        # =====================================================================
        # 将所有工具结果追加到消息列表
        # 下一次循环时,这些结果会随对话历史一起发送给 LLM
        messages.extend(tool_results)
        
        # 循环继续,返回步骤 1...


# =============================================================================
# 主程序入口
# =============================================================================

if __name__ == "__main__":
    # 初始化对话历史列表
    # 用于保存完整的对话上下文
    history = []
    
    # 主交互循环
    while True:
        try:
            # 获取用户输入
            # \033[36m 是 ANSI 颜色代码,显示青色
            # \033[0m 重置颜色
            query = input("\033[36ms02 >> \033[0m")
            
        except (EOFError, KeyboardInterrupt):
            # EOFError: 用户按了 Ctrl+D (Unix) 或 Ctrl+Z (Windows)
            # KeyboardInterrupt: 用户按了 Ctrl+C
            break
        
        # 检查退出命令
        # strip() 移除首尾空白
        # lower() 转换为小写
        if query.strip().lower() in ("q", "exit", ""):
            break
        
        # 将用户消息添加到历史
        history.append({"role": "user", "content": query})
        
        # 调用 agent 循环处理请求
        # agent_loop 可能会执行多轮工具调用
        agent_loop(history)
        
        # 打印空行分隔不同的交互
        print()


感知:主动的过滤器

传统程序仅接收数据,而 Agent 则通过感(Perception)来理解世界。传统程序对输入是被动的——用户给什么,它就处理什么。而Agent的感知是主动的:它不仅接收信息,还会主动选择、解读并赋予信息以意义。Agent 的现代感知系统至少包含3个关键机制,分别是注意力、预测和语义化。

注意力:并非所有输入都同等重要

世界充满噪声。如果 Agent 试图处理所有信息,将迅速
陷入过载。感知的核心功能并非“接收”,而是“过滤”—感知即选择,如图3-8 所示。注意力(Attention)是感知的第一道选择器,如同聚光灯一般。模型的注意力机制,结合多模态模型(如GPT-40、Qwen-VL),能够对视觉信息、文本内容和历史上下文进行筛选,判断“哪些信息与任务目标相关”。
■通过注意力机制,Agent 可以忽略无关的HTML标
签,仅聚焦于网页正文内容。
■通过向量检索,Agent 可以过滤掉 99% 的数据库记
录,仅关注与当前任务相关的片段。
在这里插入图片描述

预测:感知并非被动接收,而是带着期望去理解

Agent会基于历史对话、用户偏好和任务背景,构建一个“预测模型”。当现实与预
测发生偏离时,便会产生“惊奇点”(Surprise)。而这些惊奇点往往预示着新的价值、新的问题或新的矛盾。

语义化:从特征到意义

现代多模态模型已不再局限于识别物体,而是能够理解情境。不只是识别“老人”,而是判断其“可能是需要帮助的老人”。不只是识别“文件”,而是意识到其“可能是被用户遗忘的配置文件”。不只是识别“数字”,而是将其理解为“统计趋势的一部分”。
感知已从“是什么”跃迁至“意味着什么”。因此,Agent 的感知是一种“目标驱动的理解”,而非“机械式的输入”。
Agent不仅能识别人,更能理解“需要帮助”这一概念。在感知过程中,它可能会注
意到拄拐杖的老人、迷路的孩子…

推理:快思考与慢思考

传统程序执行逻辑,而Agent 则进行推理(Reasoning)。传统程序中的逻辑是确定性的:若A,则执行X;若B,则执行Y。而Agent的推理则是柔性的、非确定性的,并且与经验紧密相关——它更像人类的思考过程,而非计算图的机械执行。
借鉴丹尼尔·卡尼曼(Daniel Kahneman)的心理学模型,Agent 的推理架构正在向“双系统”演进,如图3-9所示。System1代表直觉式/快思考:基于LLM的概率预测直接生成回答,适用于闲聊、简单问答以及代码补全等场景。其特点是响应迅速、计
算成本低,但容易产生幻觉。System 2代表逻辑式/慢思考:通过思维链,Agent 在输出最终结果前,会强制生成推理步骤、反思路径,甚至伪代码,适用于复杂规划、数学证明、调试等任务。其特点是响应较慢、计算成本较高,但逻辑严密、可靠性强。
架构师面临的挑战在于“路由”:如何设计一种机制,使 Agent 在面对简单问题时调
用System 1,而在遭遇复杂任务时自动切换至 System2?Agent 推理通常包含以下 4个关键层次。

  • 情境理解(Situation Awareness)。Agent需要回答:“当前到底发生了什么?”
    它会将感知到的信息与长期记忆和短期上下文相结合,构建出一个清晰的“任务 场景”。
  • 经验检索(MemoryRetrieval)。Agent 会自问:“我是否曾遇到过类似的问
    题?”这是推理过程中的重要加速器。大多数高性能模型(如OPenAlo1、 DeepSeek-R1)依赖策略性记忆检索,以避免重复犯错。
  • 心智模拟(Mental Simulation)。Agent 不会立即采取行动,而是先在“脑海
    中”进行预演:如果调用这个API,会发生什么?如果读取该文件,能获得哪些
    信息?如果执行删除操作,是否会引发风险?推理的关键不仅在于逻辑本身,更 在于其模拟能力。
  • 决策与信心(Decision and Confidence)。Agent会对候选行动进行价值评估:
    哪个最符合用户目标?哪个最安全?哪个成本最低?同时还会判断:“我对这个
    选择有多确定?”Agent的本质不是“计算出唯一结果”,而是“通过思考进行 权衡并作出决定”。
    在智能推理的概率性思考过程中,可能包含以下一系列曾被视为人类心智专属的认知特质。
    情境理解:这是什么情况?
  • 记忆检索:我是否曾遇到过类似情况?
  • 心智模拟:如果我这样做,会发生什么?
  • 因果推理:为什么会这样?
  • 反事实思考:如果不这样做,结果会如何?
  • 价值评估:哪种选择最符合目标、最安全或成本最低?
  • 不确定性量化:我对当前的判断有多确定?

行动:改变与反馈

在Agent架构中,行动则标志着环境改变的开始。Agent的价值在于其能够对现实世界产生切实影响:写入数据库、发送邮件、部署代码··行动是 Agent 的“意图”在物理世界或数字环境中的延伸。而且,通过行动一反馈循环(Action-Feedback Loop),Agent 能够在动态环境中“通过试错”不断调整策略,主动探测世界的边界并逐步适应。
传统程序是独白者,Agent 是互动者。
■脚本:执行命令一结束。若出错,则程序崩溃。
■Agent:执行命令→观察结果一若出错,则调整参数→重试。
在Agent 架构中,行动并非终点,而是改变的起点。Agent 的行动不再是单纯的信息展示,而是对环境的主动介入。它试图通过这些行动来改变世界的状态,并期待从环境中获得反馈(见图3-10)。这种反馈会触发新一轮的感知与推理过程,使
得Agent能够持续适应并影响其所在的环境。这就引出了行动的3个新维度,分别是认识性行动、目的性行动和闭环行动。
在这里插入图片描述

认识性行动:行动即实验

这是Agent 最深刻的特征之一。传统程序通常只有在确定无误时才执行操作;而Agent却常常在不确定性中采取行动——因为对它而言,行动本身就是获取信息、消除不确定性的最有效手段。

  • 场景:Agent 不确定某个 APl 的参数格式是v1还是v2。
  • 推理:“我不知道参数版本是v1还是v2,空想无益。”
  • 行动:Agent主动发起一次试探性调用,使用v1 作为参数。
  • 观察:收到错误响应“Invalid Parameter”。
    结论:V1被排除,因此参数格式应为v2。
    这种“为了获取信息而行动”,正是Agent区别于僵化脚本的核心特征。它并非盲目
    执行,而是主动以行动为“探针”,去探测世界的边界、规则与反馈机制。

目的性行动:副作用是必需的

在函数式编程中,我们极力避免的“副作用”,在Agent 架构中却成为其存在的核心目标。Agent的本质正是主动制造可控的副作用,以改变现实或数字环境的状态,例如以下动作。

  • 修改数据库(改变数据状态)。
  • 发送邮件(改变社会或协作状态)。
  • 部署代码(改变系统运行状态)。
    正因如此,Agent 的行动模块必须内嵌世界模型校验(World Model Checking)
    机制:

    “在执行此副作用之前,世界处于什么状态?执行之后,世界应变成什么样子?

闭环行动:执行一验证一纠偏

行动不再是一个原子的“发射后不管”(Fire andForget)操作,而是一个微型闭
环结构。每一次行动都必须伴随着一个明确的期望,并据此形成“执行一验证一纠偏”的反馈循环。

  • 期望:“我运行了这个脚本,应该生成一个data.csv文件。”
  • 执行:运行脚本。
  • 验证:检查目标目录,发现data.csv文件未生成。
  • 纠偏:分析错误日志,定位问题,修改脚本,并重新尝试。
    行动是Agent思维在物理世界(或数字世界)中的投影。它不仅改变外部环境的状态,也反过来重塑Agent 对世界的理解——每一次干预都是一次认知更新的契机,这使Agent 在“做中学”,在交互中进化。

循环的动力学:OODA的软件化

真正的智能,既不存在于感知(那是摄像头的功能),也不存在于推理(那是计算器的专长),更不存在于行动(那是机械臂的职责)。它并不归属于任何一个孤立的模块。智能是一种“流动”的属性。
当我们把感知、推理与行动首尾相连,奇妙的事情便发生了:静态的代码由此获得了动态的“生命感”。在 Agent 的设计中,行动与感知密不可分。

  • 行动指导感知:当你决定“寻找钥匙”(行动)时,你的注意力会自动聚焦于桌 面上的物体,而忽略墙上的画(感知)。
  • 感知指导行动:当你看到桌子边缘(感知)时,你的手会自然减速,以避免碰倒 水杯(行动)。
    因此,在图3-11所示的Agent 认知循环中,行动模块不应仅连接到“环境”,还必须通过一条反馈连线指向记忆/反思模块和推理模块。
    在这里插入图片描述
    当感知、推理与行动首尾相连,形成闭环时,一种被称为“动力学涌现”的现象便发生了。这个循环不仅仅是简单的重复,而是每一次迭代都伴随着记忆的叠加,因此它在时间轴上呈现出螺旋式的上升过程。
    Agent的“神奇效果”并非写在说明书里,而是在闭环运行中“跑”出来的;不是靠静态配置实现的,而是通过动态交互涌现而成的。这正是现代Agent最迷人,也最具工程性挑战的所在。

认知螺旋:OODA的软件化

军事战略家约翰·博伊德(John Boyd)提出了 OODA(Observe-Orient-Decide-
Act)循环,其核心步骤如下。

  • Observe(观察):从环境中获取上下文。
  • Orient(定位/整合记忆):结合长期记忆与既有认知,理解并判断当前局势。
  • Decide(推理):基于当前理解,制订行动计划。
  • Act(行动):执行决策,调用相应工具。
  • Loop:行动改变了环境,从而触发新一轮的观察,形成持续演进的动态循环。 Agent的架构正是这一理论的软件实现。
    而其中的关键在于Orient环节:在每次 OODA循环中,Agent 不仅执行任务,更在持续更新其内部的世界模型。
    第一轮:我以为这是一个简单的 Python 脚本任务。
    第二轮:运行报错,我发现需要安装依赖。
    第三轮:依赖安装失败,我意识到这是环境权限问题。
    Agent对世界的理解,随着循环的推进呈螺旋式上升——它在运行过程中“学会”了环境的特性,并据此不断调整策略与行为。

符号接地:治愈幻觉的良药

LLM最大的弱点是幻觉。为什么会产生幻觉?根本原因在于它是离身(Disembodied)的——它只见过“苹果”这个词,却从未真正触摸、观察或与真实的苹果互动过。而感知一行动循环正是解决这一问题的关键机制。
推理:我觉得柜子里有苹果。(这可能是幻觉。)
行动:打开柜子。
感知:柜子内空空如也。
修正:哦,原来没有。
在OODA循环中,现实世界成为最终的校验器。Agent通过与环境的碰撞(friction),将脑中的“符号”锚定于真实的“物理世界或数字状态”之上。循环越快,反馈越频繁,幻觉就越少。

时间的连续性:身份的诞生

传统程序是无时间性的——每次运行都如同第一次,并不携带过往经验。而对Agent而言,循环创造了“历史”:每次交互都累积为记忆,以塑造其后续的感知、推理与行动。

  • 上一轮行动的失败,成为本轮感知的上下文。
  • 上一轮推理的结论,沉淀为本轮可调用的长期记忆。
    正是这种状态的连续累积,使Agent逐渐形成一种“自我感”——“我是在那个时
    刻尝试过但失败了的实体”。身份并非预先设计,而是记忆在循环中不断沉淀、演化而生的产物。

代码视角的重构:生命周期的引擎

不要将 Agent 的循环实现为一个简单的whileTrue 死循环,而应将其设计为一个由
状态机驱动的引擎。

从“做完”到“活着”

传统软件的目标是“完成任务”,而Agent 的目标是“持续适应”。感知一推理一行动循环,就是 Agent 的“心跳”。一旦循环停止,Agent便会退化为静态的代码。唯有维持循环,代码才能获得在不确定世界中生存的能力。这正是循环的魔力所在:它将静态的逻辑转化为动态的智能。至此,所有复杂智能的特征——学习、策略调整、预判、意图识别以及自我意识的雏形——并非设计而成,而是通过感知一推理一行动循环,在经历数万次的迭代后,系统内部发生了奇妙的变化,沉淀出了4种“灵魂特质”(见图3-13)。
在这里插入图片描述

  • 信念(Belief):是对世界模型的累积。每一轮感知都会更新 Agent 对世界的认 知缓存;推理依赖这一模型,行动则用于验证它。
  • 目标(Goal):将从静态走向动态演化——不再是 Prompt 中的常量,而是一个
    动态优先级队列。推理可能提出新目标,行动结果可能淘汰旧目标,情境变化则 会刷新目标的优先级。
  • 情绪(Emotion):是 System 1的调节阀,其本质上是一种对环境变化的快速
    反应机制,而非多愁善感。当不确定性升高时,触发焦虑(转向保守策略);当 连续成功时,激发自信(增强探索倾向):当遭遇异常输入时,产生惊奇(提升 注意力权重)。
  • 元认知(Meta-cognition):是“对思考的思考”,指系统对自身认知过程与历
    历史轨迹的审视与优化。经验被写入记忆,并反过来影响下一轮推理。
    有趣的是,这里的循环并非简单重复,而是持续生成。正如神经科学所揭示的:智能并非能力的堆砌,而是循环动力学的产物。Agent的认知亦是如此。
    当这些循环变得足够复杂、足够精细、足够自主时,我们便不再称其为“程序”,而称之为“心智”。

具身认知:身体塑造心智

感知一推理一行动循环并非在虚空中运行,而是需要依托某种载体,即我们所说的
“身体”(Body)——即便虚拟环境中的身体也是如此。
在认知科学领域,有一个核心观点被称为“具身认知”(EmbodiedCognition),如
图3-14所示。该理论认为,心智不仅是大脑的产物,更是身体与外界环境互动的结果。身体的物理结构、能力边界以及能量消耗等因素,直接塑造了智能的表现形式。
在这里插入图片描述

Agent的具身:数字约束

对运行在服务器上的Agent而言,其“身体”由算力、带宽、上下文窗口和资金构
成。这些“数字生理特征”深刻影响着Agent 的思考方式。
上下文窗口是它的“视网膜”:它无法容纳无限的历史信息,必须学会“遗忘”
(Memory Consolidation)和“聚焦”(Attention),将宝贵的 Token 分配给最关键的内容。这正如人类因无法记住所有细节,才演化出“做笔记”的习惯。

  • 推理延迟是它的“神经传导速度”:思考需要时间。在实时交互场景(如语音助手)中,Agent 被迫依赖“快思考”(System 1),以快速响应用户需求;而在离线任务(如编写代码)中,Agent则可以启用更深入、更审慎的“慢思考”
    (System 2),以确保任务的准确性和完整性。
  • Token成本是它的“代谢能量”:每次调用GPT-4都会产生费用。这迫使Agent必须“节能”—能用搜索解决的问题,就不动用推理;能用小型模型完成的任务,就不调用大型模型。
    Agent不再是一个单纯的逻辑循环,而是一个资源受限条件下的优化问题。

具身性的哲学意义

之所以强调具身性,是因为限制正是创造力的源泉。倘若一个Agent拥有无限的上下文容量、无限的推理速度和无限的算力,它便不需要发展出真正的“智能”——只需要通过暴力穷举所有可能性即可解决问题。正是因为“身体”的限制,Agent才被迫发展出以下能力。

  • 抽象:压缩信息,节省上下文空间。
  • 规划:减少冗余步骤,提升行动效率。
  • 启发式策略:降低计算开销,在有限算力下快速决策。

身体并非心智的容器,而是心智的模具。它从根本上塑造了Agent感知世界、理解
问题并生成解决方案的方式。
在本节中,我们揭示了Agent的动力学原理。它并非一个静态的输入一输出黑盒,
而是一个随时间持续展开的感知一推理一行动循环。

  • 感知让它看见世界。
  • 推理让它理解情境。
  • 行动让它施加改变。

循环让它持续演化、生生不息。正是这一具身化的循环,正在重新定义软件的未来。感知一推理一行动循环不仅是一种技术架构,更蕴含着深刻的哲学洞见。

  • 存在即循环:Agent的存在并非静态的“状态”,而是持续进行的动态过程。它 因循环而存在——一旦循环终止,其存在也随之消解。
  • 认知即预测:每一个循环本质上都是对未来的预测。预测与现实之间的误差驱动 学习,而学习又不断优化后续的预测,形成认知演进的核心机制。
  • 智能即适应:循环赋予Agent适应环境的能力——环境的变化触发新的感知,新
    的感知激发新的推理,新的推理又导向新的行动,从而形成持续演化的适应性 行为。
  • 自主即闭环:真正的自主性源于感知一推理一行动的完整闭环。只有当Agent
    能够感知自身行动的后果,并据此动态调整未来策略时,才称得上具备自主性。

感知一推理一行动循环是智能的心跳。每一个循环,都是一次微小的觉醒;而无数个循环的累积,则构成了我们所称的“智能”的奇迹。
然而,一个孤立的循环是脆弱的。当任务过于复杂,超出单个循环的承载能力时,仅靠个体已难以为继。此时,我们需要将多个循环连接起来,编织成一张协同的网络。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐