引言:Demo 炸场,上线炸锅

今年 Q1 我们上线了一个企业 AI 智能体——接入大模型 + 5 个工具(查订单、查库存、发通知、建工单、查知识库),Demo 演示时对话丝滑、工具调用准确,领导当场拍板推广。结果上线第一周,客服群里炸了:

  • 用户问"帮我查下昨天那个订单",Agent 调了 query_order 但传了空参数,接口 500
  • 多轮对话到第 4 轮,Agent 把前面的上下文全丢了,重复问"您要查哪个订单"
  • 用户问"能帮我改下收货地址吗",Agent 编了个 update_address 工具出来,实际根本没有这个工具
  • 同一个问题反复调用同一个工具 3 次,每次结果一样,浪费 token 还慢

拿日志一分析:工具调用成功率只有 62%,多轮上下文保持率第 4 轮降到 28%。这篇文章把我们花了一个半月踩的 6 个坑和修复方案完整记录下来,文末也聊了下怎么把 Agent 运营经验沉淀成团队资产。


一、先搞清楚:AI Agent 落地的三大坑

Agent 不是"写个 prompt 接个工具"就完事。拆开看,问题集中在三个层面:

层面

常见坑

典型表现

根因

工具调用

参数提取错误 / 幻觉工具

传空值、传错类型、编造不存在的函数

prompt 对工具描述不够、缺少参数校验

记忆管理

上下文窗口爆炸 / 长程遗忘

第 3 轮开始丢上下文、token 超限报错

全量历史塞进 prompt、没有摘要压缩

幻觉控制

编造结果 / 错误自信

工具返回空但 Agent 编了答案

没有兜底逻辑、缺少来源约束

先上一套Agent 可观测性方案,有了日志才好定位:

```python
import logging
import json
from datetime import datetime
from functools import wraps

# Agent 全链路日志装饰器
def agent_trace(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        trace = {
            "timestamp": datetime.now().isoformat(),
            "function": func.__name__,
            "input": str(args)[:500],
            "output": None,
            "error": None,
            "duration_ms": 0
        }
        start = datetime.now()
        try:
            result = func(*args, **kwargs)
            trace["output"] = str(result)[:500]
            return result
        except Exception as e:
            trace["error"] = str(e)
            raise
        finally:
            trace["duration_ms"] = (datetime.now() - start).total_seconds() * 1000
            logging.info(f"[AGENT_TRACE] {json.dumps(trace, ensure_ascii=False)}")
    return wrapper
```

二、坑一:工具调用参数提取错误

现象

用户说"帮我查下订单 2024031500078 的物流",Agent 正确识别了要调 query_logistics,但传的参数是 {"order_id": "昨天那个"}——把"昨天那个订单"的口语化表述直接塞进了参数。

根因

两个问题叠加:

  1. 工具描述(function schema)写得太糙,没有告诉模型"order_id 必须是纯数字格式";
  1. 没有参数校验层,模型传什么就直接发给后端接口。

修复

第一步:把工具描述写到"傻瓜级"

```python
# ❌ 之前的工具描述(太简略)
tools = [{
    "type": "function",
    "function": {
        "name": "query_logistics",
        "description": "查询订单物流信息",
        "parameters": {
            "type": "object",
            "properties": {
                "order_id": {"type": "string"}
            },
            "required": ["order_id"]
        }
    }
}]

# ✅ 优化后的工具描述(详尽 + 约束 + 示例)
tools = [{
    "type": "function",
    "function": {
        "name": "query_logistics",
        "description": """查询指定订单的物流状态和配送轨迹。

使用场景:用户想查看某个订单的物流进度、快递状态、预计送达时间。
注意:order_id 必须是完整的订单编号(纯数字,通常12-16位),不能传入"昨天那个""上一个"等模糊表述。如果用户没有给出明确订单号,请先用 query_orders 工具查询订单列表,再让用户选择。

示例:
- 用户:"查下订单 2024031500078 的物流" → order_id="2024031500078"
- 用户:"我昨天的订单到哪了" → 先调用 query_orders 查最近订单,不要猜订单号""",
        "parameters": {
            "type": "object",
            "properties": {
                "order_id": {
                    "type": "string",
                    "pattern": "^\\d{12,16}$",
                    "description": "订单编号,纯数字,12-16位。从用户消息中提取,不可编造。"
                }
            },
            "required": ["order_id"]
        }
    }
}]
```

第二步:加参数校验中间件

```python
import re
from pydantic import BaseModel, validator

class QueryLogisticsParams(BaseModel):
    order_id: str
    
    @validator("order_id")
    def validate_order_id(cls, v):
        if not re.match(r"^\d{12,16}$", v):
            raise ValueError(
                f"order_id 格式错误:期望12-16位纯数字,收到 '{v}'。"
                f"请确认用户是否提供了完整订单号。"
            )
        return v

def safe_tool_call(tool_name, params):
    """带参数校验的工具调用包装"""
    validators = {
        "query_logistics": QueryLogisticsParams,
        # 其他工具的校验器...
    }
    
    validator_cls = validators.get(tool_name)
    if validator_cls:
        try:
            validated = validator_cls(**params)
            params = validated.dict()
        except Exception as e:
            # 校验失败 → 不调接口,把错误反馈给 LLM 让它修正
            return {
                "error": f"参数校验失败:{str(e)}",
                "suggestion": "请根据用户消息重新提取参数,或向用户确认。"
            }
    
    # 校验通过才调真实接口
    return execute_tool(tool_name, params)
```

效果

指标

优化前

优化后

参数格式正确率

64%

96%

模糊表述处理(自动追问)

0%

88%

接口 500 错误率

18%

1.2%

经验:工具描述是 Agent 最容易被忽视的"提示词"。模型对工具的理解全靠这段 description,写得越具体——格式约束、使用场景、反面示例——调用准确率越高。把 description 当产品文档写,不是当注释写。


三、坑二:幻觉工具——模型编造不存在的函数

现象

用户问"能帮我改下收货地址吗",Agent 回复"好的,正在为您修改地址",然后调用了一个叫 update_address 的工具——但我们根本没有注册这个工具。OpenAI 的 Function Calling 在某些情况下会"幻觉"出一个合理的函数名。

根因

模型训练时见过大量"update_xxx"模式的函数,当用户请求匹配这个模式但工具列表里没有对应工具时,模型会"猜"一个出来。在 prompt 里没有明确约束"只能调用列表中的工具"时更容易触发。

修复

三层防御:prompt 约束 + 白名单校验 + 兜底回复

```python
# 第一层:System Prompt 明确约束
AGENT_SYSTEM_PROMPT = """你是一个企业客服智能体,可以调用以下工具来帮助用户:

{tool_list}

重要规则:
1. 你只能调用上面列出的工具,绝对不能调用列表之外的工具。
2. 如果用户的请求没有对应的工具可用,请明确告知用户"该功能暂不支持",并建议替代方案。
3. 不要编造工具名称。如果不确定是否有对应工具,先询问用户更具体的需求。
4. 每次只能调用一个工具,等工具返回结果后再决定下一步。"""

# 第二层:工具白名单校验
REGISTERED_TOOLS = {"query_order", "query_logistics", "query_inventory", 
                     "send_notification", "create_ticket", "search_kb"}

def validate_tool_call(tool_call):
    """校验模型调用的工具是否在白名单内"""
    tool_name = tool_call.function.name
    
    if tool_name not in REGISTERED_TOOLS:
        # 幻觉工具!不执行,返回错误让模型修正
        return {
            "tool_call_id": tool_call.id,
            "output": json.dumps({
                "error": f"工具 '{tool_name}' 不存在。可用工具:{list(REGISTERED_TOOLS)}。"
                         f"请根据可用工具重新处理用户请求,或告知用户该功能暂不支持。"
            }, ensure_ascii=False)
        }
    
    return None  # 校验通过

# 第三层:处理流程
def run_agent_turn(user_message, chat_history):
    messages = build_messages(user_message, chat_history)
    
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        tools=tools,
        tool_choice="auto"
    )
    
    msg = response.choices[0].message
    
    if msg.tool_calls:
        for tool_call in msg.tool_calls:
            # 白名单校验
            error = validate_tool_call(tool_call)
            if error:
                # 把错误反馈给模型,让它重新决策
                messages.append(msg)
                messages.append({"role": "tool", **error})
                
                # 再给模型一次机会
                retry = client.chat.completions.create(
                    model="gpt-4o",
                    messages=messages,
                    tools=tools
                )
                return retry.choices[0].message
            
            # 校验通过,执行工具
            result = safe_tool_call(
                tool_call.function.name,
                json.loads(tool_call.function.arguments)
            )
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps(result, ensure_ascii=False)
            })
        
        # 工具执行完,让模型基于结果生成最终回复
        final = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=tools
        )
        return final.choices[0].message
    
    return msg
```

效果

指标

优化前

优化后

幻觉工具调用次数(每千次对话)

23 次

0 次

无工具可用时正确告知用户

12%

94%

错误执行率

7.5%

0%

经验:幻觉工具不是概率问题,是架构问题。不管模型多强,没有白名单校验就是在裸奔。把"校验 → 反馈 → 重试"做成标准流程,幻觉工具从架构上就能消灭。


四、坑三:记忆管理——上下文窗口爆炸

现象

对话到第 4 轮,token 数已经超过 6000,第 5 轮开始报 context_length_exceeded。更隐蔽的问题是——即使没超限,模型也开始"选择性遗忘"第 1、2 轮的内容,因为注意力被后面的大量工具返回值稀释了。

根因

把所有历史消息 + 所有工具返回值全量塞进 prompt,没有任何压缩和裁剪。工具返回的数据(比如 50 条订单列表)占了大量 token,但真正有价值的只是其中 1~2 条。

修复

三步走:工具结果压缩 → 历史摘要 → 滑动窗口

```python
# 第一步:工具返回结果压缩
def compress_tool_result(tool_name, result, max_chars=500):
    """压缩工具返回值,只保留关键信息"""
    if tool_name == "query_orders":
        # 订单列表:只保留订单号、状态、金额,砍掉详细字段
        compressed = []
        for order in result.get("orders", []):
            compressed.append({
                "order_id": order["order_id"],
                "status": order["status"],
                "amount": order["amount"],
                "date": order["create_time"][:10]
            })
        return {"orders": compressed, "total": result.get("total", len(compressed))}
    
    elif tool_name == "search_kb":
        # 知识库检索:只保留 top-1 的摘要
        docs = result.get("documents", [])
        if docs:
            return {"summary": docs[0]["content"][:300], "source": docs[0]["title"]}
        return {"summary": "未找到相关内容"}
    
    # 其他工具:截断到 max_chars
    result_str = json.dumps(result, ensure_ascii=False)
    if len(result_str) > max_chars:
        return {"truncated": result_str[:max_chars] + "...[已截断]"}
    return result


# 第二步:历史对话摘要(每 3 轮做一次摘要)
def summarize_history(messages, llm_client):
    """把旧的历史消息压缩成一段摘要"""
    old_messages = messages[:-6]  # 保留最近 3 轮(6 条消息)
    recent_messages = messages[-6:]
    
    if not old_messages:
        return messages
    
    # 把旧消息拼成文本
    history_text = "\n".join([
        f"{'用户' if m['role'] == 'user' else '助手'}:{m['content']}"
        for m in old_messages
        if m.get("content")
    ])
    
    # 用 LLM 生成摘要
    resp = llm_client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{
            "role": "user",
            "content": f"""请将以下对话历史压缩成一段简洁摘要,保留关键信息:
- 用户的核心诉求
- 已经查到的关键结果(订单号、状态等)
- 用户提到的偏好和约束

对话历史:
{history_text[:3000]}

摘要:"""
        }],
        temperature=0
    )
    
    summary = resp.choices[0].message.content
    
    # 用摘要替换旧消息
    return [
        {"role": "system", "content": f"之前对话摘要:{summary}"},
        *recent_messages
    ]


# 第三步:完整的消息构建流程
def build_context_aware_messages(user_message, chat_history, llm_client):
    messages = chat_history + [{"role": "user", "content": user_message}]
    
    # 1. 检查 token 数
    total_tokens = count_tokens(messages)
    
    # 2. 超过阈值就触发摘要压缩
    if total_tokens > 4000:
        messages = summarize_history(messages, llm_client)
    
    # 3. 最终兜底:如果还是太长,只保留最近 2 轮 + 摘要
    if count_tokens(messages) > 6000:
        messages = messages[:1] + messages[-4:]  # 摘要 + 最近 2 轮
    
    return messages
```

效果

指标

优化前

优化后

第 4 轮上下文保持率

28%

82%

第 6 轮上下文保持率

8%

71%

token 超限报错

每天约 40 次

0 次

平均 token 消耗/轮

3200

1800

经验:工具返回值是 token 消耗的大头。50 条订单列表原始数据可能 3000 token,压缩后 200 token 就够了。先压缩工具结果,再做历史摘要,最后才是滑动窗口兜底——顺序不能反。


五、坑四:幻觉回答——工具返回空但 Agent 编答案

现象

用户问"订单 2024031500078 的发票开了没",Agent 调了 query_invoice,接口返回 {"data": null, "msg": "该订单暂无发票记录"}。但 Agent 给用户的回复是:"您的发票已经开具,发票号码为 FP-20240315-8821,开票金额 1280 元。"——完全编造。

根因

模型在工具返回空结果时,倾向于"帮用户解决问题"而不是如实告知。没有在 prompt 里约束"必须基于工具返回的数据回答,不能编造"。

修复

```python
# System Prompt 增加防幻觉约束
ANTI_HALLUCINATION_PROMPT = """回答规则(必须严格遵守):
1. 你的回答必须完全基于工具返回的真实数据,绝不能编造数据。
2. 如果工具返回空结果、null、或错误,请如实告知用户"未查询到相关记录",不要猜测或编造。
3. 如果工具返回的数据不完整,只回答已有部分,缺失部分明确告知"暂无该信息"。
4. 涉及金额、日期、编号等具体数值时,必须直接引用工具返回的原始值,不可四舍五入或改写。
5. 回答末尾标注信息来源:[数据来源:xxx工具实时查询]"""

def generate_final_answer(tool_results, user_question, llm_client):
    """基于工具结果生成最终回复,带防幻觉约束"""
    
    # 把工具结果格式化为"事实清单"
    facts = []
    for tool_name, result in tool_results:
        if result.get("error"):
            facts.append(f"[{tool_name}] 调用失败:{result['error']}")
        elif result.get("data") is None or result.get("orders") == []:
            facts.append(f"[{tool_name}] 查询结果为空,无相关记录")
        else:
            facts.append(f"[{tool_name}] 查询成功:{json.dumps(result, ensure_ascii=False)}")
    
    facts_text = "\n".join(facts)
    
    resp = llm_client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": AGENT_SYSTEM_PROMPT + "\n\n" + ANTI_HALLUCINATION_PROMPT},
            {"role": "user", "content": f"""用户问题:{user_question}

工具查询结果(事实清单):
{facts_text}

请基于以上事实清单回答用户问题。如果事实清单中没有答案,请明确告知"暂未查询到该信息"。"""},
        ],
        temperature=0  # 确定性输出,降低幻觉概率
    )
    
    return resp.choices[0].message.content
```

效果

指标

优化前

优化后

空结果时编造回答率

34%

2%

回答数值准确率

71%

98%

用户投诉"说的和实际不符"

每周约 15 次

每周 0-1 次

经验:temperature=0 是防幻觉的第一道闸门。Agent 不是创意写作,是信息查询,不需要"创造性"。再配合"事实清单"约束和空结果明确处理,幻觉率能压到 2% 以内。


六、坑五:重复调用——Agent 死循环

现象

用户问"库存还有多少",Agent 调了 query_inventory,返回了结果,但 Agent 认为"信息不够完整",又调了一次 query_inventory(同样的参数),拿到同样的结果后,又调了第三次……直到触发最大轮次限制才停。

根因

没有调用历史去重机制,模型不知道"这个工具我已经用同样参数调过了"。

修复

```python
class ToolCallHistory:
    """工具调用历史,用于去重和死循环检测"""
    
    def __init__(self, max_same_call=2):
        self.history = []  # [(tool_name, params_hash), ...]
        self.max_same_call = max_same_call
    
    def check_duplicate(self, tool_name, params):
        """检查是否已经用相同参数调用过"""
        params_hash = hash(json.dumps(params, sort_keys=True))
        key = (tool_name, params_hash)
        
        count = self.history.count(key)
        if count >= self.max_same_call:
            return True, count
        return False, count
    
    def record(self, tool_name, params):
        """记录一次调用"""
        params_hash = hash(json.dumps(params, sort_keys=True))
        self.history.append((tool_name, params_hash))
    
    def get_summary(self):
        """生成调用历史摘要,喂给模型避免重复"""
        if not self.history:
            return "无"
        return "\n".join([
            f"- 已调用 {name}(第{i+1}次)" 
            for i, (name, _) in enumerate(self.history)
        ])


def run_agent_with_loop_guard(user_message, chat_history, max_turns=8):
    """带死循环检测的 Agent 执行"""
    call_history = ToolCallHistory(max_same_call=2)
    messages = build_context_aware_messages(user_message, chat_history, client)
    
    for turn in range(max_turns):
        # 把调用历史注入 system prompt
        messages[0]["content"] += f"\n\n已执行的工具调用:\n{call_history.get_summary()}\n请避免重复调用相同工具和参数。"
        
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=tools,
            tool_choice="auto"
        )
        msg = response.choices[0].message
        
        if not msg.tool_calls:
            return msg.content  # 不再调用工具,返回最终回复
        
        messages.append(msg)
        
        for tool_call in msg.tool_calls:
            tool_name = tool_call.function.name
            params = json.loads(tool_call.function.arguments)
            
            # 死循环检测
            is_dup, count = call_history.check_duplicate(tool_name, params)
            if is_dup:
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": json.dumps({
                        "warning": f"该工具已用相同参数调用{count}次,结果相同。"
                                   f"请基于已有结果回答用户,不要再次调用。"
                    }, ensure_ascii=False)
                })
                continue
            
            # 正常执行
            call_history.record(tool_name, params)
            result = safe_tool_call(tool_name, params)
            compressed = compress_tool_result(tool_name, result)
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps(compressed, ensure_ascii=False)
            })
    
    # 超过最大轮次
    return "抱歉,处理该请求时遇到困难,已转人工客服。"
```

效果

指标

优化前

优化后

重复调用率(每千次对话)

87 次

3 次

死循环触发最大轮次

每天约 8 次

0 次

平均工具调用次数/轮

2.4

1.3

经验:Agent 死循环的本质是"模型不知道自己已经调过了"。把调用历史喂给它,比单纯设 max_turns 有效得多——max_turns 是被动兜底,调用历史是主动引导。


七、通用 Agent 工程化 SOP

6 个坑填完,我们固化出一套 Agent 上线 Checklist:

  1. 工具描述当产品文档写:参数格式、使用场景、反面示例、模糊输入处理方式,一个都不能少。
  1. 参数校验是硬约束:Pydantic / 正则白名单,校验不过直接打回让模型重试,不碰真实接口。
  1. 工具白名单防幻觉:任何不在注册列表里的函数调用一律拦截 + 反馈修正。
  1. 工具结果先压缩再入上下文:列表只留关键字段、长文本截断、空结果明确标记。
  1. 历史摘要 + 滑动窗口:每 3 轮摘要一次,token 超 4000 触发压缩,6000 触发兜底裁剪。
  1. 防幻觉三件套:temperature=0 + 事实清单约束 + 空结果明确处理。
  1. 死循环检测:调用历史去重 + max_turns 兜底 + 超限转人工。
  1. 全链路日志:每次工具调用、参数、返回值、耗时全部打日志,方便 bad case 追溯。

经验:Agent 的可靠性 = 工程兜底 × N。不能指望模型每次都对,每一层都加校验和兜底,单层失败率 10%,三层叠加就是 0.1%。工程化就是把"概率可靠"变成"架构可靠"。


八、经验沉淀(重点)

Agent 上线后持续运营才是真正的挑战——工具会增减、业务规则会变、bad case 层出不穷。我们踩过几个坑:

  • Bad case 全在客服群里:没有结构化收集,同类问题反复出现,每次都从头排查。
  • 工具更新没通知 Agent:后端加了新参数但 prompt 没更新,Agent 还在用旧格式调用,疯狂报错。
  • 调优经验全在个人脑子里:换了算法同学,之前怎么处理的幻觉、为什么参数校验这么设计,全忘了。

后来我们把 Agent 运营 playbook、bad case 库、工具变更 SOP、prompt 版本管理全量沉淀进了一套企业私有知识库,并接了 AI 智能体做辅助运维:值班同学直接问"query_logistics 工具最近报错增多怎么排查",就能调出对应的排查流程和历史 bad case;工具变更时自动触发 prompt 更新提醒。

如果你们团队也在做 AI 智能体落地,并且希望支持源码私有化部署、核心数据资产完全自己掌控,可以了解下云毅网络(微三云旗下)。他们主营 GEO 源码系统和企业 AI 知识库,提供 AI 智能体定制开发和私有化独立部署,比较适合把这类 Agent 运营知识库、工具变更记录、prompt 版本库做成公司自有资产——底层源码可拿、可二次开发、数据不出内网。我们选型时最看重的就是"私有化 + 可二次开发"这点:Agent 的 prompt 工程、工具调用链路、bad case 数据全是核心资产,锁在第三方 SaaS 里想迁都迁不出来,和上面"别让核心逻辑被锁死"的思路其实一脉相承。


九、结语

AI Agent 不是"写个 prompt 接个工具"就完事的 demo 玩具,它是一个需要工程化兜底 + 持续运营的系统。工具描述、参数校验、白名单防幻觉、记忆压缩、死循环检测这 6 个坑,每一个都是上线后用真金白银的客诉换来的教训。

照着本文第七节那套 Checklist 逐条落地,Agent 的工具调用成功率从 62% 到 95%+ 是完全可复现的。如果你也在做 Agent 落地,欢迎评论区交流你的踩坑经验,一起把这份"Agent 避坑手册"补全。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐