AI Agent实战指南:从工具调用到自主智能体

前言:Agent 不是更好的聊天机器人

很多人对 Agent 的理解停留在"会调工具的 LLM",但真正做下来才发现:Agent 的核心难点不是调工具,而是任务规划、状态管理、错误恢复这一堆工程问题。

简单对话的边界很容易摸到——模型再聪明,没有手脚也只能给你写备忘录。Agent 给模型装上了"手脚"和"记忆",让它能真正完成任务。但代价是工程复杂度成倍上升。

一、Agent 的核心组成

一个完整的 Agent 系统包含四个核心模块:

graph LR A[用户请求] --> B[意图理解] B --> C[任务规划] C --> D[工具执行] D --> E[结果整合] E --> F[响应生成] C --> G[记忆管理] G --> C D --> H[错误处理] H --> C
模块职责关键问题
意图理解解析用户到底想要什么歧义、隐含需求
任务规划把目标拆成可执行步骤线性 vs 分层、依赖关系
工具执行调用外部能力工具设计、错误处理
记忆管理管理上下文和历史短期 vs 长期、上下文窗口

二、从 API 调用到工具调用

2.1 基础 API 调用

最开始的 LLM 应用就是一行 API 调用:

from openai import OpenAI

client = OpenAI(api_key="sk-xxx")

def chat(message):
    response = client.chat.completions.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": "你是一个有用的助手。"},
            {"role": "user", "content": message}
        ]
    )
    return response.choices[0].message.content

这个版本能对话,但问题很多:

  1. 没记忆,每次对话都是独立的
  2. 没结构化输出,想提取内容还得自己解析
  3. 没错误处理,网络异常、超时、限流都是直接挂
  4. 没成本控制,用户聊多了 Token 费用不可控

2.2 加上记忆:滑动窗口

要让模型"记住"之前的对话,最简单的做法是把历史消息都塞进去:

from collections import deque

class ChatSessionWithWindow:
    def __init__(self, window_size=6):
        self.messages = deque(maxlen=window_size)
        self.messages.append(
            {"role": "system", "content": "你是一个有用的助手。"}
        )

    def chat(self, message):
        self.messages.append({"role": "user", "content": message})
        response = client.chat.completions.create(
            model="gpt-4",
            messages=list(self.messages)
        )
        answer = response.choices[0].message.content
        self.messages.append({"role": "assistant", "content": answer})
        return answer

2.3 分层记忆策略

真正生产环境用的记忆策略要分层:

class MemoryAssistant:
    def __init__(self):
        self.system_prompt = "你是一个专业的编程助手。"
        self.long_term_memory = {}  # key: user_id, value: summary
        self.conversation_history = {}  # key: session_id, value: deque

    def get_system_messages(self, user_id):
        messages = [{"role": "system", "content": self.system_prompt}]
        if user_id in self.long_term_memory:
            messages.append({
                "role": "system",
                "content": f"用户背景:{self.long_term_memory[user_id]}"
            })
        return messages

三种记忆的实践:

  • 短期记忆:当前对话内的上下文,LangChain 自动管理
  • 长期记忆:跨对话的持久化记忆,用向量数据库存储
  • 上下文记忆:当前任务的临时状态,用 Python 对象管理

三、工具调用:Agent 的"手脚"

3.1 Function Calling 基础

2023 年底 OpenAI 推出 Function Calling(后来改名成 Tools),才算有了标准方案。

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_current_weather",
            "description": "获取指定地点的当前天气",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "城市名称,例如:北京、上海"
                    }
                },
                "required": ["location"]
            }
        }
    }
]

response = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": "北京今天天气怎么样?"}],
    tools=tools
)

tool_calls = response.choices[0].message.tool_calls

3.2 工具设计的三个坑

坑一:工具粒度过细

# 错误示例:工具太细碎
tools = [
    Tool(name="ReadFile", ...),
    Tool(name="WriteFile", ...),
    Tool(name="DeleteFile", ...),
    Tool(name="MoveFile", ...),
    Tool(name="CopyFile", ...),
]

结果模型在规划任务时会过度纠结,把简单任务拆得很复杂。

坑二:工具描述太泛

# 错误:描述太宽泛
Tool(
    name="ReadFile",
    func=read_file,
    description="读取本地文件内容,输入文件路径"
)

# 正确:描述精确
Tool(
    name="ReadFile",
    func=read_file,
    description="读取指定的本地文件内容。必须提供完整、有效的文件路径。只能读取文本文件,不能浏览目录或执行文件操作。"
)

坑三:工具输出格式不统一

class ToolResult:
    def __init__(self, success, data, error=None):
        self.success = success
        self.data = data
        self.error = error

def read_file(filepath):
    try:
        with open(filepath, 'r') as f:
            content = f.read()
        return ToolResult(True, {'content': content})
    except Exception as e:
        return ToolResult(False, None, error=str(e))

3.3 工具执行器封装

实际项目里推荐用统一的执行器:

from concurrent.futures import ThreadPoolExecutor

class ToolExecutor:
    def __init__(self):
        self.tools = {}

    def register(self, name: str, description: str, parameters: dict):
        def decorator(func):
            self.tools[name] = {
                "function": func,
                "description": description,
                "parameters": parameters
            }
            return func
        return decorator

    def execute(self, tool_call) -> str:
        name = tool_call.function.name
        args = json.loads(tool_call.function.arguments)

        try:
            result = self.tools[name]["function"](**args)
            return json.dumps({"success": True, "result": result})
        except Exception as e:
            return json.dumps({"success": False, "error": str(e)})

四、任务规划:Agent 的"大脑"

4.1 线性规划的局限

最开始的 Agent 都是线性规划——模型看到任务,直接输出一系列步骤。简单场景够用,复杂任务就撑不住。

比如:“分析 CSV 文件,根据销售额排名生成报表,邮件发送给团队”

1. 读取 CSV 文件
2. 计算销售额排名
3. 生成图表
4. 发送邮件

问题:第 2 步失败怎么办?CSV 格式不对怎么办?邮件发送失败要不要重试?

4.2 分层规划

更稳的做法是把任务拆成三层:

class TaskPlanner:
    def plan(self, user_request):
        # 第一层:理解目标
        goal = self.understand_goal(user_request)

        # 第二层:拆分子任务
        subtasks = self.breakdown_goal(goal)

        # 第三层:为每个子任务规划具体步骤
        execution_plans = {}
        for subtask in subtasks:
            execution_plans[subtask.id] = self.plan_execution(subtask)

        return {
            'goal': goal,
            'subtasks': subtasks,
            'execution_plans': execution_plans,
            'dependencies': self.analyze_dependencies(subtasks)
        }

分层规划的好处:

  • 可观察性:你知道模型在哪个层次上思考
  • 可干预:子任务失败时,可以局部调整而不影响整体
  • 可扩展:新类型任务只需要扩展对应的层次

4.3 AutoGPT 的陷阱

AutoGPT 的"永不放弃"精神看起来很励志,但实际操作中就是资源浪费。

典型问题:执行某个子任务时遇到依赖冲突,AutoGPT 尝试了几次失败后开始循环尝试不同方案,20 多分钟后消耗大量 API 调用,最后依然卡住。

解决方案:加合理的停止条件

MAX_TASK_ATTEMPTS = 3
MAX_TOTAL_STEPS = 20
STEP_TIMEOUT_SECONDS = 60

class TaskExecutor:
    def should_continue(self, task_id):
        # 单个任务尝试次数检查
        if self.task_attempts.get(task_id, 0) >= MAX_TASK_ATTEMPTS:
            return False, f"任务 {task_id} 已达到最大尝试次数"

        # 总步骤数检查
        if self.total_steps >= MAX_TOTAL_STEPS:
            return False, f"已达到最大步骤数"

        # 超时检查
        if time.time() - self.last_reset_time > STEP_TIMEOUT_SECONDS:
            return False, f"任务执行超时"

        return True, ""

核心教训:不要高估模型的自主能力。 给模型太多自由度反而会降低可靠性。

五、LangChain Agent 实践

5.1 LangChain Agent 基础

from langchain.tools import Tool
from langchain.agents import initialize_agent, AgentType
from langchain.chat_models import ChatOpenAI

# 定义工具
def get_github_latest_release(repo_name):
    import requests
    url = f"https://api.github.com/repos/{repo_name}/releases/latest"
    response = requests.get(url)
    return response.json().get('tag_name', 'Unknown')

tools = [
    Tool(
        name="GitHubLatestRelease",
        func=get_github_latest_release,
        description="获取 GitHub 仓库的最新版本号,输入格式为 'owner/repo'"
    )
]

# 初始化 Agent
llm = ChatOpenAI(model="gpt-4", temperature=0)
agent = initialize_agent(
    tools=tools,
    llm=llm,
    agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION,
    verbose=True
)

result = agent.run("LangChain 的最新版本是多少?")

5.2 从 LangChain 到 LangGraph

LangChain 的 Agent 在状态管理上经常出问题。LangGraph 提供了 StateGraph,更适合复杂状态流。

from typing import TypedDict, Annotated, List
from langgraph.graph import StateGraph, END
import operator

class AgentState(TypedDict):
    messages: Annotated[List, operator.add]
    retrieved_docs: List[str]
    current_step: int
    max_steps: int
    tool_calls: List[str]
    final_answer: str

# 构建工作流
workflow = StateGraph(AgentState)

# 添加节点
workflow.add_node("intent_recognition", intent_recognition)
workflow.add_node("smart_retrieval", smart_retrieval)
workflow.add_node("tool_execution", tool_execution)
workflow.add_node("answer_generation", answer_generation)
workflow.add_node("satisfaction_check", satisfaction_check)

# 添加边
workflow.set_entry_point("intent_recognition")
workflow.add_edge("intent_recognition", "smart_retrieval")
workflow.add_edge("smart_retrieval", "tool_execution")
workflow.add_edge("tool_execution", "answer_generation")
workflow.add_edge("answer_generation", "satisfaction_check")

# 条件边:根据满意度决定是否重新检索
workflow.add_conditional_edges(
    "satisfaction_check",
    lambda x: "smart_retrieval" if x.get("status") != "completed" else END,
    {
        "smart_retrieval": "smart_retrieval",
        END: END
    }
)

app = workflow.compile()

LangGraph 的优势:

  • 状态显式定义,不会丢失
  • 节点间的数据流清晰
  • 支持条件分支和循环
  • 便于调试和定位问题

六、Agentic RAG:让 RAG 会思考

传统 RAG 像个死板的搜索引擎——你问什么,它就按固定套路检索什么。Agentic RAG 让 RAG 能像人一样做决策和规划。

6.1 核心能力

  • 动态检索策略:根据问题复杂度选择不同检索方式
  • 多步推理:复杂问题分解成子问题
  • 工具调用:能调用外部工具(数据库、API、计算器)
  • 自我反思:结果不满意时调整策略

6.2 意图识别节点

def intent_recognition(state: AgentState):
    last_message = state["messages"][-1]
    response = llm.invoke(intent_prompt.format(question=last_message.content))
    intent = json.loads(response.content)

    state["current_step"] = 0
    state["max_steps"] = 5 if intent["need_steps"] else 1

    return {
        **state,
        "strategy": intent["strategy"]  # vector_search / hybrid_search / multi_step
    }

6.3 满意度评估节点

def satisfaction_check(state: AgentState):
    answer = state["final_answer"]
    score = evaluate_answer(answer)

    if score < 3 and state["current_step"] < state["max_steps"]:
        # 不满意,换策略重新检索
        state["current_step"] += 1
        return {**state, "strategy": "hybrid_search"}
    else:
        return {**state, "status": "completed"}

6.4 Agentic RAG 的效果

上线两个月的实测数据:

  • 准确率从 65% 提升到 82%
  • 复杂问题解决率从 40% 提升到 75%
  • 用户满意度从 3.2 分提升到 4.1 分(满分 5 分)

七、NDJSON:Agent 的流式协议

7.1 为什么 Agent 离不开 NDJSON

Agent 系统的核心特征:长时间运行 + 多阶段事件 + 可观测

如果所有中间态塞进一个最终 JSON,会有几个现实问题:

  1. 交互体验差:用户要干等几十秒才看到回复
  2. 进度不可见:工具调用、报错来不及展示
  3. 失败难定位:任务中途挂了,手里没结构化轨迹
  4. 桥接层难写:IM、Web UI、日志系统都想边收边处理

NDJSON(Newline Delimited JSON)刚好卡住这个痛点:一行一条记录,每行本身必须是合法 JSON。

{"ts":"2026-07-20T10:00:01+08:00","type":"message","text":"帮我查一下明天日程"}
{"ts":"2026-07-20T10:00:02+08:00","type":"tool_call","name":"calendar.search"}
{"ts":"2026-07-20T10:00:03+08:00","type":"tool_result","ok":true,"count":2}
{"ts":"2026-07-20T10:00:04+08:00","type":"final","text":"明天有两场会"}

7.2 事件协议设计

type 做事件协议,而不是一个大而全 schema:

import json
import sys

def emit(obj: dict) -> None:
    sys.stdout.write(json.dumps(obj, ensure_ascii=False) + "\n")
    sys.stdout.flush()  # 很关键,不 flush 对端可能看不到事件

# 发送不同类型的事件
emit({"type": "ready", "session_id": "s_01"})
emit({"type": "text_delta", "delta": "正在"})
emit({"type": "text_delta", "delta": "查询"})
emit({"type": "final", "text": "正在查询日程。"})

增量文本用 delta,最终再用一条 final 兜底。

这样 UI 可以边拼边显示,存储层又不用猜"现在拼到哪了"。

7.3 坏行隔离

生产环境的事件流难免脏,要做行级容错:

import json
import logging

logger = logging.getLogger(__name__)

def iter_events(fp):
    for lineno, raw in enumerate(fp, 1):
        line = raw.strip()
        if not line or line.startswith("#"):
            continue
        try:
            yield json.loads(line)
        except json.JSONDecodeError:
            logger.warning("skip bad ndjson line=%s content=%r", lineno, line[:200])

JSON 数组中间坏一块,常常整包都解析失败;NDJSON 可以天然做行级容错。

7.4 Unix 工具友好

NDJSON 的一大优势:用普通 Unix 工具就能抽样、过滤、排障。

# 看最后 20 条事件
tail -n 20 run.ndjson

# 只看工具调用
rg '"type":"tool_call"' run.ndjson

# 统计事件类型分布
rg -o '"type":"[^"]+"' run.ndjson | sort | uniq -c | sort -nr

# 抽 100 条做本地复现
head -n 100 run.ndjson > sample.ndjson

一次失败的 run 留下一份 NDJSON 轨迹,比只留最终报错更值钱。

八、错误处理:让 Agent 知道何时求助

8.1 主动错误处理

不要等错误发生后再处理,要在规划阶段就预想可能的错误:

def safe_tool_call(tool_name, tool_func, **kwargs):
    try:
        result = tool_func(**kwargs)
        if not result.success:
            return {
                'status': 'error',
                'error_type': 'tool_execution_error',
                'message': f"工具 {tool_name} 执行失败: {result.error}"
            }
        return {'status': 'success', 'data': result.data}
    except Exception as e:
        return {
            'status': 'error',
            'error_type': 'unexpected_error',
            'message': f"工具 {tool_name} 遇到意外错误: {str(e)}"
        }

8.2 人工干预策略

有些错误模型自己解决不了,需要人工干预:

class HumanInterventionHandler:
    def __init__(self):
        self.intervention_threshold = 3  # 同类错误 3 次后请求人工干预

    def handle_error(self, error_type, error_context, error_count):
        if error_count >= self.intervention_threshold:
            return self.request_human_help(error_type, error_context)
        else:
            return self.suggest_recovery(error_type, error_context)

    def suggest_recovery(self, error_type, error_context):
        recovery_strategies = {
            'tool_execution_error': [
                '检查工具参数是否正确',
                '验证工具依赖环境是否正常',
                '尝试使用备用工具'
            ],
            'planning_error': [
                '重新评估任务目标',
                '增加子任务的颗粒度',
                '补充必要的任务步骤'
            ]
        }
        return {
            'action': 'retry_with_suggestion',
            'suggestions': recovery_strategies.get(error_type, ['重新尝试'])
        }

8.3 错误重试策略

模型调用失败是常态,重试策略要 carefully 设计:

from tenacity import retry, stop_after_attempt, wait_exponential

@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=2, max=10)
)
def call_model_with_retry(prompt):
    return llm.predict(prompt)

注意:不是所有错误都应该重试。参数错误不应该重试,配额不足需要特殊处理。

九、成本控制

9.1 成本爆炸的常见原因

  1. 工具调用循环:模型陷入死循环,反复调用工具
  2. 上下文膨胀:对话历史塞太多,每次调用 Token 都很多
  3. 模型选择不当:简单任务也用大模型

9.2 控制策略

分级模型策略:

def get_model_for_task(complexity: str):
    if complexity == "simple":
        return "gpt-4o-mini"  # 简单问题用小模型
    elif complexity == "complex":
        return "gpt-4o"  # 复杂问题才用大模型

答案缓存:

from functools import lru_cache

@lru_cache(maxsize=100)
def get_cached_answer(question_hash: str) -> str:
    # 从缓存获取答案
    pass

# 在意图识别前先查缓存
question_hash = hashlib.md5(question.encode()).hexdigest()
cached = get_cached_answer(question_hash)
if cached:
    return cached

Token 计数限制:

import tiktoken

def count_tokens(text, model="gpt-4"):
    encoding = tiktoken.encoding_for_model(model)
    return len(encoding.encode(text))

def chat_with_limit(message, max_tokens=1000):
    input_tokens = count_tokens(message)
    if input_tokens > max_tokens:
        return "问题太长,请简化后重试。"
    # ...

十、上下文窗口管理

10.1 上下文爆炸

模型上下文窗口有限。一次复杂任务中,Agent 处理多个大文件,每个几千行代码,默认记忆管理会把所有工具调用历史都塞进上下文,结果很快就爆了。

10.2 上下文压缩策略

class ContextAwareMemory:
    def __init__(self, max_tokens=4000):
        self.max_tokens = max_tokens
        self.conversation_history = []

    def add_message(self, role, content, importance=1.0):
        message = {'role': role, 'content': content, 'importance': importance}
        self.conversation_history.append(message)
        self._prune_if_needed()

    def _prune_if_needed(self):
        current_tokens = sum(len(msg['content']) for msg in self.conversation_history)
        if current_tokens > self.max_tokens:
            # 按重要性排序,移除低重要性的消息
            sorted_messages = sorted(
                self.conversation_history,
                key=lambda x: x['importance']
            )
            # 保留最重要的 80%
            keep_count = int(len(sorted_messages) * 0.8)
            self.conversation_history = sorted_messages[-keep_count:]

10.3 对话历史压缩

把连续的工具调用压缩成摘要:

def compress_conversation(messages):
    compressed = []
    i = 0
    while i < len(messages):
        if messages[i]['role'] == 'assistant' and i + 2 < len(messages):
            if messages[i+1]['role'] == 'tool' and messages[i+2]['role'] == 'assistant':
                compressed.append({
                    'role': 'assistant',
                    'content': f"[工具调用: {messages[i]['content'][:50]}... -> 回应: {messages[i+2]['content'][:50]}...]"
                })
                i += 3
                continue
        compressed.append(messages[i])
        i += 1
    return compressed

十一、实战经验总结

11.1 几个关键教训

折腾了一圈 Agent 开发,几个比较实在的体会:

  1. 不要高估模型的自主能力:给模型太多自由度反而降低可靠性。适度约束和人工干预是必要的。

  2. 工具设计比模型能力更重要:工具描述要精确但不过度限制;工具输入要严格验证;工具输出要统一格式;工具粒度要适中。

  3. 任务规划是核心能力:Agent 的智能程度主要体现在任务规划上,而不是单个工具的执行上。

  4. 错误处理要主动:不要等错误发生后再处理,要在规划阶段就预想可能的错误。

  5. 记忆管理要精简:区分长期和短期记忆,对内容做重要性标注,定期压缩历史。

11.2 任务复杂度与成功率

实际项目中的典型数据:

任务类型成功率主要失败原因
简单任务(“检查服务器状态”)95%+边缘情况
中等任务(“重启所有后端服务”)~80%边缘情况、参数错误
复杂任务(“诊断性能问题”)~60%需要人工干预

11.3 何时该用 Agent

适合用 Agent 的场景:

  • 任务需要多步骤、多工具配合
  • 任务有明确的子目标划分
  • 错误可以检测和恢复

不适合用 Agent 的场景:

  • 任务可以用确定性算法解决
  • 任务对延迟极其敏感
  • 任务错误成本极高

记住:不是所有任务都需要 Agent,有时候规则系统更简单可靠。

十二、写在最后

Agent 开发不是在造一个"能代替人"的系统,而是在造一个"能辅助人"的系统。它的价值不在于完全自动化,而在于把那些重复的、机械的任务自动化,让人能专注于需要判断和决策的部分。

如果你也在折腾 Agent 开发,建议:

  1. 从小处开始:先把一个简单场景做扎实
  2. 不要追求完全自主:适度的约束和人工干预是正常的
  3. 工具设计和任务规划比模型微调更重要
  4. 把重点放在可靠性和可观测性上,而不是炫酷的功能

Agent 开发还处在早期阶段,很多问题还没有标准答案。但这恰恰是有意思的地方——我们在和一个新兴的技术一起成长。

只要别让 Agent 把你的服务器删了就行。


本文整合了 7 篇 AI Agent 应用开发文章,涵盖工具调用、任务规划、LangChain/LangGraph、Agentic RAG、NDJSON 流式协议、错误处理、成本控制等核心技术。

版权声明: 本文首发于 指尖魔法屋-AI Agent实战指南:从工具调用到自主智能体https://blog.thinkmoon.cn/post/ai-agent-comprehensive-guide/) 转载或引用必须申明原指尖魔法屋来源及源地址!