生成式 AI · Bedrock

从「能聊」到「能办事」,中间隔着权限与可观测性

智能体真正难的不是模型调工具,是怎么保证它不会越权、不会重复下单、出错时能追溯并回滚。

落地过的智能体场景

云成本助手

自然语言问「上月哪个服务涨得最多」,自动查 Cost Explorer 并给出归因与优化建议。

工单智能分派

读工单内容自动分类、判定优先级、关联历史相似单,直接分派到对口小组。

运维排障助手

告警触发后自动拉指标、查日志、比对变更记录,产出初步根因假设推给值班群。

合同与文档审阅

对照审阅清单逐条检查,标出风险条款与缺失项,输出结构化审阅意见。

数据查询助手

自然语言转 SQL,在只读副本上执行,返回结果与图表,全程记录审计日志。

内部知识问答

结合 RAG 与工具调用,既能回答制度问题,也能直接帮你提申请、查进度。

工具调用循环(含权限与审批)

python agent_loop.py
"""智能体工具调用循环。

设计要点:
    1. 工具分级:只读工具直接执行,写操作工具需人工审批
    2. 幂等键:每次写操作带幂等键,防止重试造成重复执行
    3. 步数上限:防止模型陷入死循环烧钱
    4. 全链路审计:每步的输入输出都落库
"""

from __future__ import annotations

import json
import uuid
from typing import Any, Callable

import boto3

BEDROCK = boto3.client("bedrock-runtime", region_name="ap-northeast-1")
MODEL = "anthropic.claude-3-5-sonnet-20241022-v2:0"
MAX_TURNS = 8

# ---------- 工具注册表 ----------

TOOL_SPECS = [
    {
        "toolSpec": {
            "name": "query_monthly_cost",
            "description": "查询指定月份、指定维度的 AWS 成本。只读操作。",
            "inputSchema": {"json": {
                "type": "object",
                "properties": {
                    "month": {"type": "string", "description": "格式 YYYY-MM"},
                    "group_by": {"type": "string", "enum": ["SERVICE", "ACCOUNT", "TAG"]},
                },
                "required": ["month", "group_by"],
            }},
        }
    },
    {
        "toolSpec": {
            "name": "create_optimization_ticket",
            "description": "创建一张成本优化工单。写操作,需人工审批。",
            "inputSchema": {"json": {
                "type": "object",
                "properties": {
                    "title": {"type": "string"},
                    "detail": {"type": "string"},
                    "estimated_saving": {"type": "number"},
                    "owner": {"type": "string"},
                },
                "required": ["title", "detail", "owner"],
            }},
        }
    },
]

READ_ONLY_TOOLS = {"query_monthly_cost"}


def execute_tool(
    name: str,
    args: dict[str, Any],
    registry: dict[str, Callable],
    approve: Callable[[str, dict], bool],
    idem_key: str,
) -> dict[str, Any]:
    """执行工具,写操作先过审批。"""
    if name not in registry:
        return {"error": f"未注册的工具:{name}"}

    if name not in READ_ONLY_TOOLS:
        if not approve(name, args):
            return {"error": "人工审批未通过,操作已取消", "requires_approval": True}
        args = {**args, "idempotency_key": idem_key}

    try:
        return {"result": registry[name](**args)}
    except Exception as exc:  # noqa: BLE001 - 工具异常需回传给模型
        return {"error": f"{type(exc).__name__}: {exc}"}


def run_agent(
    user_input: str,
    registry: dict[str, Callable],
    approve: Callable[[str, dict], bool],
    audit: Callable[[dict], None],
) -> dict[str, Any]:
    """主循环。返回最终答复与完整调用轨迹。"""
    messages: list[dict[str, Any]] = [
        {"role": "user", "content": [{"text": user_input}]}
    ]
    trace: list[dict[str, Any]] = []
    session_id = str(uuid.uuid4())

    for turn in range(MAX_TURNS):
        resp = BEDROCK.converse(
            modelId=MODEL,
            messages=messages,
            toolConfig={"tools": TOOL_SPECS},
            inferenceConfig={"maxTokens": 2000, "temperature": 0.2},
            system=[{"text": (
                "你是云成本运营助手。先查数据再给结论,"
                "涉及创建工单等写操作时,必须先说明理由和预估收益。"
            )}],
        )

        msg = resp["output"]["message"]
        messages.append(msg)
        stop = resp["stopReason"]

        audit({
            "session": session_id, "turn": turn,
            "stop_reason": stop, "usage": resp["usage"],
        })

        if stop != "tool_use":
            text =".join(b.get("text",") for b in msg["content"])
            return {"answer": text, "trace": trace, "turns": turn + 1}

        tool_results = []
        for block in msg["content"]:
            if "toolUse" not in block:
                continue
            tu = block["toolUse"]
            idem = f"{session_id}:{turn}:{tu['name']}"
            out = execute_tool(tu["name"], tu["input"], registry, approve, idem)
            trace.append({"tool": tu["name"], "input": tu["input"], "output": out})
            tool_results.append({"toolResult": {
                "toolUseId": tu["toolUseId"],
                "content": [{"json": out}],
                "status": "error" if "error" in out else "success",
            }})

        messages.append({"role": "user", "content": tool_results})

    return {"answer": "达到最大步数上限,任务未完成。", "trace": trace, "turns": MAX_TURNS}

MAX_TURNS 是必须的:没有步数上限的智能体,一个死循环能在几分钟内烧掉不少 token 费用。

智能体上线前必查

权限边界

  • 工具对应的 IAM 权限最小化,只读与写操作分开角色
  • 写操作强制人工审批或二次确认
  • 所有写操作带幂等键,防止重试造成重复执行
  • 敏感数据字段在返回给模型前脱敏

可靠性

  • 设置步数上限与总 token 预算上限
  • 工具超时与重试策略明确
  • 工具异常以结构化错误返回给模型,而不是直接抛出
  • 关键流程有回滚路径

可观测性

  • 每次会话记录完整调用轨迹(输入、工具、输出)
  • token 消耗按用户与业务线归属
  • 异常率、平均步数、审批通过率做成看板
  • 用户反馈按钮,负反馈自动进人工复核队列

下一步

把云的复杂度交给我们,你只管做业务

留下需求,沐杉云的解决方案架构师会在一个工作日内联系你,提供免费的现状评估、迁移方案草案与 TCO 测算表。