AI Agent 开发实战:从工具调用到自动化工作流

2026-05-03 20:54 AI Agent 开发实战:从工具调用到自动化工作流已关闭评论

AI Agent 开发实战:从工具调用到自动化工作流

用 Claude API 搭自动化周报生成器的时候踩了个坑——单次 Prompt 调用根本搞不定多步骤任务。工具调用(Tool Use)加上循环执行,才是让 AI 真正"干事"的关键。

问题是什么

需求其实很简单:每周自动从 Jira 拉项目数据,分析进度风险,生成带图表的周报,再发到企业微信。

一开始我就写了一个 Prompt 调 Claude API,塞一堆数据进去让它一次性输出。结果惨不忍睹——Token 全花在记数据上,分析浅得像流水账,图表更是完全没法看。

痛点很明确:LLM 的单次调用是无状态的。你给数据,它吐回答,然后就没了。它不会说"等等,我先查一下昨天的数据",也不会说"让我再验证一下这个数字对不对"。要让 AI 搞定复杂的多步骤任务,必须上 Agent 模式——让模型能主动调工具、看结果、自己决定下一步。

解决思路

我对比了三种方案:

方案 原理 复杂度 适用场景
纯 Prompt 工程 手写 CoT 引导模型分步推理 简单分析任务
LangChain Agent 框架封装了 Tool Use + Agent Loop 快速原型、标准流程
手写 Agent Loop 自己控制 API 调用循环,完全自定义 需要精细控制、定制化工具

选了第三种——手写 Agent Loop。原因很简单:我要完全控制工具的调用逻辑和错误重试策略,不想被框架的黑盒卡住。生产环境自己管错误处理和状态,心里更有底。

操作步骤

步骤 1:定义工具 Schema

Agent 的根基是工具调用。先把需要用到的工具定义成 JSON Schema,这是 Anthropic API 的标准格式:

tools = [
    {
        "name": "fetch_jira_sprint_data",
        "description": "获取当前 Sprint 的 Jira 任务数据,包括任务状态、负责人、预估工时",
        "input_schema": {
            "type": "object",
            "properties": {
                "sprint_id": {
                    "type": "string",
                    "description": "Sprint ID"
                }
            },
            "required": ["sprint_id"]
        }
    },
    {
        "name": "analyze_risk",
        "description": "分析任务列表中的进度风险,返回高风险项",
        "input_schema": {
            "type": "object",
            "properties": {
                "tasks": {
                    "type": "array",
                    "items": {"type": "object"},
                    "description": "任务列表"
                }
            },
            "required": ["tasks"]
        }
    },
    {
        "name": "generate_chart",
        "description": "生成进度的燃尽图(Burndown Chart),返回图片 URL",
        "input_schema": {
            "type": "object",
            "properties": {
                "data": {
                    "type": "object",
                    "description": "燃尽图数据"
                }
            },
            "required": ["data"]
        }
    },
    {
        "name": "send_wecom_message",
        "description": "发送消息到企业微信群机器人",
        "input_schema": {
            "type": "object",
            "properties": {
                "content": {
                    "type": "string",
                    "description": "消息内容(支持 Markdown)"
                }
            },
            "required": ["content"]
        }
    }
]

Tool Schema 里的 description 字段特别关键——模型靠它来判断什么时候调哪个工具,写得越清楚,模型选得越准。

步骤 2:实现工具函数

每个 Schema 对应一个真实的 Python 函数,这些就是 Agent 的"手和脚":

import json
import requests
from datetime import datetime, timedelta

def fetch_jira_sprint_data(sprint_id: str) -> dict:
    """从 Jira API 获取 Sprint 数据"""
    # 实际项目中这里用 Jira API 认证调用
    # 这里用模拟数据演示
    return {
        "sprint_id": sprint_id,
        "tasks": [
            {"id": "PROJ-101", "title": "用户登录重构", "status": "进行中", 
             "assignee": "张三", "estimate_hours": 16, "remaining_hours": 10},
            {"id": "PROJ-102", "title": "支付模块对接", "status": "阻塞", 
             "assignee": "李四", "estimate_hours": 24, "remaining_hours": 20,
             "block_reason": "等待第三方接口文档"},
            {"id": "PROJ-103", "title": "首页性能优化", "status": "已完成", 
             "assignee": "王五", "estimate_hours": 8, "remaining_hours": 0},
        ]
    }

def analyze_risk(tasks: list) -> dict:
    """分析任务风险"""
    high_risk = []
    medium_risk = []
    for task in tasks:
        if task.get("status") == "阻塞":
            high_risk.append(task)
        elif task.get("remaining_hours", 0) > task.get("estimate_hours", 1) * 0.7:
            medium_risk.append(task)
    return {
        "high_risk_count": len(high_risk),
        "medium_risk_count": len(medium_risk),
        "high_risk_tasks": high_risk,
        "overall_status": "需关注" if high_risk else "正常"
    }

def generate_chart(data: dict) -> str:
    """生成燃尽图,返回图片 URL(这里用 matplotlib 生成)"""
    import matplotlib.pyplot as plt
    # 实际代码会画图并上传到图床
    # 此处省略具体绘图代码
    return "https://example.com/charts/burndown_sprint_123.png"

def send_wecom_message(content: str) -> dict:
    """发送企业微信消息"""
    webhook_url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY"
    payload = {
        "msgtype": "markdown",
        "markdown": {"content": content}
    }
    resp = requests.post(webhook_url, json=payload)
    return {"status": "ok" if resp.status_code == 200 else "fail"}

步骤 3:实现 Agent 主循环

核心来了——Agent Loop。每次 API 调用后检查响应里有没有 tool_use 类型的 content block,有就执行对应工具,把结果塞回消息列表,然后继续调 API:

import anthropic

client = anthropic.Anthropic(api_key="sk-ant-xxx")

def run_agent(prompt: str, max_iterations: int = 10):
    """Agent 主循环"""
    messages = [{"role": "user", "content": prompt}]
    
    # 工具名称 -> 函数的映射
    tool_map = {
        "fetch_jira_sprint_data": fetch_jira_sprint_data,
        "analyze_risk": analyze_risk,
        "generate_chart": generate_chart,
        "send_wecom_message": send_wecom_message,
    }
    
    for i in range(max_iterations):
        print(f"
=== Agent 迭代 {i + 1} ===")
        
        response = client.messages.create(
            model="claude-sonnet-4-20250506",
            max_tokens=4000,
            system="你是一个周报生成助手。请按顺序执行:"
                   "1. 获取 Jira Sprint 数据 "
                   "2. 分析进度风险 "
                   "3. 生成燃尽图 "
                   "4. 整理周报内容 "
                   "5. 发送到企业微信。每次只调用一个工具。",
            messages=messages,
            tools=tools
        )
        
        # 检查是否有工具调用
        has_tool_call = False
        for block in response.content:
            if block.type == "tool_use":
                has_tool_call = True
                tool_name = block.name
                tool_input = block.input
                tool_use_id = block.id
                
                print(f"→ 调用工具: {tool_name}")
                print(f"  参数: {json.dumps(tool_input, ensure_ascii=False)}")
                
                # 执行工具函数
                try:
                    result = tool_map[tool_name](**tool_input)
                    result_str = json.dumps(result, ensure_ascii=False)
                except Exception as e:
                    result_str = json.dumps({"error": str(e)})
                
                print(f"  结果: {result_str[:200]}...")
                
                # 将工具调用和结果追加到消息列表
                messages.append({
                    "role": "assistant",
                    "content": [block]
                })
                messages.append({
                    "role": "user",
                    "content": [
                        {
                            "type": "tool_result",
                            "tool_use_id": tool_use_id,
                            "content": result_str
                        }
                    ]
                })
            elif block.type == "text" and not has_tool_call:
                # 没有工具调用时,说明 Agent 完成了任务
                return block.text
        
        if not has_tool_call:
            break
    
    # 返回最后一次响应的文本
    for block in reversed(response.content):
        if block.type == "text":
            return block.text
    return "Agent 未生成有效响应"

步骤 4:添加状态管理和错误重试

生产环境最大的坑——API 可能挂,工具可能抛异常。我加了两层保护:

import time
from functools import wraps

def retry_on_failure(max_retries=3, delay=1):
    """API 调用重试装饰器"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except anthropic.RateLimitError:
                    wait = delay * (2 ** attempt)
                    print(f"⚠ 触发限流,{wait}秒后重试...")
                    time.sleep(wait)
                except anthropic.APIError as e:
                    print(f"⚠ API 错误: {e}")
                    if attempt == max_retries - 1:
                        raise
                    time.sleep(delay)
            return None
        return wrapper
    return decorator

@retry_on_failure()
def call_llm(messages, tools):
    return client.messages.create(
        model="claude-sonnet-4-20250506",
        max_tokens=4000,
        system="你是一个周报生成助手。",
        messages=messages,
        tools=tools
    )

限流在生产环境很常见,指数退避是最基础的重试策略,配合请求排队和并发控制就更稳了。

步骤 5:组装完整工作流

把上面所有模块串起来:

def weekly_report_workflow():
    """完整的周报生成工作流"""
    print("🚀 启动周报生成工作流")
    
    prompt = """请生成本周 Sprint 周报:
1. 先获取当前 Sprint 数据(Sprint ID: SPRINT-23)
2. 分析任务进度风险
3. 生成燃尽图
4. 整理成周报 Markdown 格式
5. 发送到企业微信"""

    result = run_agent(prompt)
    
    if result:
        print("
✅ 周报生成完成!")
        print(result)
    else:
        print("
❌ 周报生成失败")

if __name__ == "__main__":
    weekly_report_workflow()

结果与总结

这套 Agent 架构上线跑了 6 周,效果比单次 Prompt 调用好太多:

  • **周报质量明显提升**:Agent 先拉数据、再分析、再生成,每一步聚焦一个任务,输出比一次性生成深得多
  • **自主决策能力**:有次 Jira API 返回空数据,Agent 没直接报错,自己决定"重试一次",第二次成功才继续——这行为我并没有显式编码
  • **可追溯**:每次工具调用都有完整日志,哪个步骤出问题一目了然
  • 踩到的坑:

  • **Token 消耗翻倍**:Agent Loop 每次迭代都要把历史消息全传进去,6 次迭代的周报任务一次消耗约 30K tokens。用 `claude-sonnet-4-20250506` 成本还能接受,换 Opus 就肉疼了
  • **工具调用顺序不可控**:System Prompt 里写了"先获取数据再分析",但有时候模型会并发调两个工具(API 支持一次返回多个 tool_use)。需要额外逻辑控制串行执行
  • **无限循环风险**:有次工具返回的数据格式和模型预期不符,模型反复调同一个工具就是不输出最终结果。必须设 `max_iterations` 上限保底
  • 延伸思考

    这个 Agent Loop 是最朴素的 ReAct 模式,生产级 Agent 还可以做几件事:

  • **记忆持久化**:把跨轮次的关键信息存到向量数据库或 KV 存储,避免重复调工具获取相同信息
  • **子 Agent 编排**:复杂任务拆成多个子 Agent(比如"数据分析 Agent"、"图表生成 Agent"),通过 Orchestrator Agent 统一调度——每个 Agent 专注一件事,准确率更高
  • **流式输出**:现在的实现等全部完成才输出,可以改成 SSE 流式推送每一步的中间结果,体验好很多
  • **人机协作(Human-in-the-Loop)**:高风险操作(比如"发消息到生产环境")可以设计成 Agent 先输出待审批内容,等人工确认再执行——这是企业落地的关键
  • 工具调用让 LLM 从"聊天机器人"进化成了"能干活的 Agent",Agent Loop 让 AI 能完成真正的工作流。这个模式不复杂,核心就是两件事:定义好工具接口,写对循环逻辑。框架能帮你省点代码,但理解了底层原理,你才能真正驾驭 Agent。

    你可能感兴趣的文章

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

    资源分享

    分类:Android 标签:
    修改猎豹浏览器主页与IE浏览器之间的区别 修改猎豹浏览器主页与IE浏览器
    第一次使用Android Studio的感受 第一次使用Android Studio的感
    uiautomator2命令行实例 uiautomator2命令行实例
    Ubuntu系统Use a production WSGI server instead Ubuntu系统Use a production W

    评论已关闭!