# Agent67 幻觉问题：根因与解决方案

> 日期: 2026-04-03 ~ 2026-04-04

---

## 1. 问题描述

agent67 通过 AgentPaaS 或 `run.py` 执行编程任务时，出现严重的**幻觉**：

- 声称"所有测试通过"但从未运行测试
- 声称"已提交推送"但没有执行 git 命令
- 编造完整的项目分析（Flask 电商后端）但实际文件是 Java 数据结构作业
- 工具调用失败后不重试，直接编造成功结果

---

## 2. 根因分析

### 2.1 核心原因：`claude -p` 无状态模式

agent67 的 LLM 后端 `ClaudeLam` 使用 `claude -p`（pipe mode）调用 Claude：

```
每一步 ReAct:
  subprocess.run(["claude", "-p", "--system-prompt", prompt], input=state)
                 ↑ 新进程，无上下文记忆
```

**每一步都是独立的子进程调用**。Claude 没有前几步的记忆，必须从 state 字符串重建上下文。当 state 被压缩或截断时，关键信息丢失，导致 LLM：

1. 不知道之前读过哪些文件
2. 不知道工具调用失败了
3. 重复读同一个文件（死循环）
4. 凭空编造之前"做过"的操作

### 2.2 五个具体因素

| # | 因素 | 影响 |
|---|------|------|
| 1 | **工具输入格式不匹配** | `from_config` 传 dict，PersonalAssistant 传 JSON string。dict 路径跳过了 `_parse_input` 的容错逻辑，导致 `VALIDATION_ERROR` |
| 2 | **MCP 工具泄漏** | `claude -p` 继承当前 Claude Code 会话的 MCP 配置（Vercel/Gmail），LLM 认为自己只有这些工具，拒绝使用 system prompt 中声明的工具 |
| 3 | **观察结果被截断** | `PersonalAssistant` 的 observations 累积后过长，`context_manager.compact()` 压缩丢失关键内容；`from_config` 的 `_MAX_OBS_LENGTH=800` 导致 `find` 输出被截断 |
| 4 | **stop_condition bug** | `result[-200:]` 检查截掉了短结果中的 `[Step` 标记，导致误判为最终回复 |
| 5 | **无终止验证** | LLM 调用 `terminate` 时可以在 summary 中编造任意内容，没有验证声称的操作是否真正执行 |

### 2.3 两条执行路径对比

```
PersonalAssistant 路径 (run.py):
  用户 → PersonalAssistant.chat()
       → ClaudeLam("claude -p") → LLM 输出
       → parse_and_execute() 硬编码路由
       → BUILTIN_TOOLS[name].apply(json_string)

from_config 路径 (agentpaas chat):
  用户 → POST /agents/{id}/run
       → from_config("agent-config.yml") → Loop(react_step)
       → ClaudeLam("claude -p") → LLM 输出
       → _extract_tool_call() → _timeout_call(tool, dict)
```

PersonalAssistant 路径最初没有幻觉，是因为 `json.dumps()` 序列化工具输入，恰好绕过了 schema 验证的严格模式。

---

## 3. 解决方案

### 3.1 最终方案：ConversationLam + 统一 Provider

**根本修复**：引入 ConversationLam 包装层，将任意 LLMProvider 从无状态改为有状态。不再局限于 Claude Code 的 `--resume`，而是对所有 Provider 统一提供完整对话历史。

```python
class ConversationLam:
    """将任意 LLMProvider 包装为有状态的对话调用"""
    def __init__(self, provider: LLMProvider):
        self.provider = provider
        self.messages = []  # 完整对话历史

    def send(self, content: str, role: str = "user") -> str:
        self.messages.append({"role": role, "content": content})
        response = self.provider.chat(self.messages)
        self.messages.append({"role": "assistant", "content": response})
        return response
```

不同 Provider 的底层实现各异，但对 ConversationLam 透明：

| Provider | 底层会话机制 |
|----------|------------|
| **ClaudeCodeProvider** | 首次调用获取 `session_id`，后续用 `--resume` 恢复会话 |
| **OpenAICompatProvider** | 每次调用传完整 `messages` 数组（适用于 OpenAI / DashScope / DeepSeek 等） |

react_step 因此大幅简化：

```python
def react_step(state, step_index):
    if step_index == 0:
        response = conversation.send(full_input)       # 完整输入
    else:
        response = conversation.send(latest_observation) # 只发最新 observation
    # provider 内部已持有完整历史，无需 state 压缩/滑窗
```

**效果**：

| | 无状态 (`-p`) | 有状态 (ConversationLam) |
|---|---|---|
| 上下文 | 每步从 state 字符串重建 | Provider 保持完整会话记忆 |
| 文件内容 | 被压缩/截断后可能丢失 | LLM 记得之前读过的所有文件 |
| 工具调用历史 | 依赖 state 中的 `[Step N]` 标记 | LLM 记得每一步的操作和结果 |
| 死循环 | 常见（反复读同一文件） | 极少（LLM 知道已经读过了） |
| 幻觉 | 频繁（丢失上下文后编造） | 零幻觉（完整记忆，已验证） |
| Provider 支持 | 仅 Claude Code | Claude Code / OpenAI / DashScope / DeepSeek 等全部支持 |

### 3.2 辅助修复（仍然有价值）

即使有了会话持久化，以下修复仍然保留以提供额外防护：

#### MCP 隔离
```bash
claude -p --mcp-config '{"mcpServers":{}}' --strict-mcp-config
```
防止 Claude 看到 Vercel/Gmail 等无关 MCP 工具。**所有路径都需要**。

#### 工具输入序列化（P0）
```python
# from_config 路径：dict → JSON string，与 PersonalAssistant 对齐
if isinstance(tool_input, dict):
    tool_input_val = json.dumps(tool_input, ensure_ascii=False)
```

#### 工具参数别名
```python
# _parse_input: LLM 常用 "path" 代替 "file_path"
_ALIASES = {"path": "file_path", "filepath": "file_path", ...}
```

#### 自动工具参数文档（P1）
```python
# 从 schema 自动生成，注入 system prompt
- **ReadFile**: `{"action":"ReadFile","input":{"file_path": ..., "offset": 0, "limit": 2000}}`
- **Bash**: `{"action":"Bash","input":{"command": ..., "timeout": 120}}`
```

#### 观察滑窗（PersonalAssistant）
```python
# 只保留最近 5 步完整结果，旧的压缩为摘要
if len(observations) > 5:
    # 旧步骤：一行摘要
    # 最近 5 步：完整内容
```

#### 工具名提示
```python
# --resume 后 Claude 倾向于用内置工具名 (Read/Write/Edit)
"工具名是 ReadFile(不是Read)、WriteFile(不是Write)、EditFile(不是Edit)"
```

---

## 4. 使用方式

### 推荐：统一入口（ConversationLam，所有 Provider 均无幻觉）
```bash
python3 -m agentpaas chat lambda
```

### 指定 Provider
```bash
# Claude Code 后端（使用 --resume 会话持久化）
python3 -m agentpaas chat lambda --provider claude-code

# OpenAI 兼容后端（传完整 messages 数组）
python3 -m agentpaas chat lambda --provider openai-compat
```

### PersonalAssistant 路径（仍可用）
```bash
python3 agentexample/agent67/run.py --claude
```

### 带 PaaS 追踪
```bash
AGENTPAAS_API_KEY=<key> python3 agentexample/agent67/launch_paas.py chat
```

---

## 5. 关键代码变更

| 文件 | 变更 |
|------|------|
| `lambdagent/providers/claude_code.py` | ClaudeLam 会话持久化（`_session_id` + `--resume`） |
| `lambdagent/providers/__init__.py` | 新包，导出 ClaudeLam |
| `agentexample/agent67/core/assistant.py` | `inject_override=False`、observations 滑窗、工具名提示、max_steps=30 |
| `agentexample/agent67/core/claude_lam.py` | Re-export from lambdagent.providers |
| `agentexample/agent67/run.py` | `--claude` 参数 |
| `lambdagent/fromconfig/compiler.py` | P0-P3 修复、工具参数文档、terminate 验证 |
| `lambdagent/builtin_tools/file_tools.py` | ListFiles 默认 pattern、参数别名 |

---

## 6. 经验教训

1. **无状态 LLM 调用不适合多步任务** — 每步重建上下文的信息损失是幻觉的根本原因
2. **会话持久化是最有效的反幻觉手段** — 比 prompt 工程、输出验证、关键词检测都更可靠
3. **Prompt 层面的防幻觉规则效果有限** — LLM 会用各种变体措辞绕过关键词检测
4. **工具 schema 文档很重要** — LLM 不知道参数名就会猜，猜错导致 VALIDATION_ERROR，进而触发幻觉
5. **MCP 工具隔离必须全局生效** — 不隔离会导致 LLM 认知混乱
