附录 F · FAQ 与排错
按现象组织,而非按章节。每条都回跳到正文细节。
F.1 安装与启动
"ImportError: cannot import name 'Agentao'"
- 检查是否装了包(
uv add agentao或pip install agentao,不是只装openai) - 从顶层导入:
from agentao import Agentao(别走from agentao.agent import Agentao——那条路径不保证稳定)
"No module named 'openai' / 'mcp'"
需要 MCP 时带上 extras:
uv add 'agentao[mcp]' # 或
uv add 'agentao[all]'"ValueError: Agentao(): api_key, base_url, and model are required…"
直接调用 Agentao(...) 不读任何环境变量,所以凭据必须传进去。两种做法:
- 直接传:
Agentao(api_key=…, base_url=…, model=…, working_directory=…),或者用预构造的llm_client=代替这三个。 - 走工厂函数:
agentao.embedding.build_from_environment(working_directory=…)会从进程环境变量和<working_directory>/.env读取OPENAI_API_KEY/OPENAI_BASE_URL/OPENAI_MODEL(或LLM_PROVIDER对应的{PROVIDER}_*)。
走工厂时,关键字参数 > 进程环境变量 > .env。见 附录 B。
"Model 'gpt-5.4' not found"(自建端点)
默认模型 id 是 gpt-5.4。如果你的端点提供别的模型,传 model= 或设 OPENAI_MODEL。见 2.2。
F.2 运行时行为
所有写操作都被"已取消"
你设了 PermissionMode.READ_ONLY(显式或默认)。两条路:
- 以可写模式启动:
agent = Agentao(working_directory=workdir, permission_mode="workspace-write", ...);或对运行中的 agent 调agent.set_permission_mode("workspace-write")(它同时拨动只读的两个开关;裸调permission_engine.set_mode(...)只拨一个)。用自己的引擎时改传permission_engine=,不要和permission_mode=同时传。见 5.4 - 或在 transport 上实现
confirm_tool,让用户交互确认
chat() 永不返回
三个可能原因:
- 工具死循环 —— 撞到
max_iterations。调低上限或接on_max_iterations(4.6) - 工具卡住 —— 自定义工具无超时。用
timeout=包住子进程/HTTP 调用(6.7) - 需要用户输入 —— 默认
ask_user在无头模式会永久等待。用SdkTransport(ask_user=…)override
宿主侧硬兜底:
reply = await asyncio.wait_for(agent.arun(msg), timeout=120)"我的工具拿到了奇怪的路径"
execute() 收到的是 LLM 给的原样参数。校验参数,并用 self._resolve_path(raw) 把相对路径接到 working_directory 下——见 Tool 基类。
输出里有奇怪的转义序列
终端不支持颜色码。要么:
- 显示前在 transport 侧关掉颜色(
rich.console.Console(no_color=True)) - 加后处理过滤;Agentao 本身不强制任何颜色策略
F.3 记忆与会话
"我清了历史但老上下文还在"
clear_history() 只清 self.messages。memory DB 是特意保留的。要把 memory 一起清:
agent.clear_history()
agent.memory.clear(scope="project")记忆跨租户泄漏
经典多租户陷阱——你把 ~/.agentao/memory.db 的 user 作用域跨租户挂了。两条路:
- 固定每租户的 working directory 并禁用 user 作用域;或
- user 作用域用
tenant_id+user_id做 key
见 6.4。
"重启后会话丢了"
两处要修:
- SDK:自己序列化
agent.messages;重启后agent.messages = saved_messages - ACP:用
session/load+ 存好的sessionId——agent 必须声明loadSession: true(7.2 模式)
F.4 MCP
"MCP 服务器声明了但看不到工具"
按顺序排查:
/mcp命令(或agent.mcp_manager.get_status())——服务器是ready吗?- 子进程 stderr——服务器把日志打到 stdout 会破坏帧格式
- 工具名冲突——同名
{server}_{tool}注册两次会在agentao.log里警告
"'mcp' 命令找不到"
装 MCP extras。如果 MCP 服务器是 npx 启动的,Linux 上可能还要装 Node。
"服务器 timeout"
三层:
mcp.json的timeout。写数字("timeout": 30)只限制连接和握手,默认 60 秒;写{"startup": 60, "request": 30}才同时限制每次工具调用- 不写
request时,stdio 上的工具调用没有上限;SSE 和 Streamable HTTP 上要连续约 300 秒收不到 server 的数据才会断开 - 外层
asyncio.wait_for
最严格那一层胜出。见 附录 B.3.1。
F.5 安全与沙箱
"macOS 报 sandbox-exec 拒绝"
看 agentao.log——具体原因有记录。常见修法:
- Shell profile 太严 → 把
default_profile从readonly换成workspace-write-no-network - 命令跑到 workspace 外了 →
working_directory内用绝对路径 - 见 6.2
"生产上有人把沙箱关了——如何强制"
沙箱配置只来自 ~/.agentao/sandbox.json。项目里的 .agentao/sandbox.json 会被忽略并记 warning,所以仓库带的或 agent 写的文件都关不掉沙箱。容器里让用户级文件对 agent 只读;用 agentao doctor 查看被忽略的项目文件。见 7.4 陷阱表。
"Agent 试图抓 169.254.169.254"
正常——SSRF 尝试会被内置黑名单拦。agentao.log 里会有 deny 记录;核对 PermissionEngine 规则(6.3)。
F.6 ACP 集成
initialize 时报 handshake_fail
多半是版本不匹配。Agentao v0.2.x 讲 protocolVersion: 1(整数)。如果你的客户端发 "2025-09-01" 这样的字符串,服务器会拒。见 3.1。如果错误到你手里是 AcpRpcError(不是带 code=HANDSHAKE_FAIL 的 AcpClientError),则握手阶段信号在 details["phase"] == "handshake"——完整分类规则见附录 D §D.7。
prompt_once 报 server_busy
Fail-fast 语义——已有别人在 turn 里。send_prompt 同样是 fail-fast,manager 也没有队列。几条路:
- 先用
readiness(name)做门禁,再等 + 重试 - 在宿主里自己排队
- 或每租户独立子进程
见 附录 D.6 和 3.4 每个 server 只有一个活跃 turn。
"session/cancel 停不下我的长工具"
取消通过 CancellationToken 传播,但你的自定义工具必须配合。长循环内定期读 self._current_token 并 token.check()。
"现在这个 ACP server 能不能接单?"
不要去字符串匹配 state,调用 readiness(name):
if mgr.is_ready("my-server"):
mgr.prompt_once("my-server", "hello", timeout=30)"ready"—— 可以直接提交。"busy"—— 当前已有 turn 在跑,此时提交会得到SERVER_BUSY。"failed"—— server 已粘性 fatal,或处于failed且进程从未启动或已退出,需要运维显式调restart_server()/start_server()。(可恢复的死亡不会落到这里:自动恢复会重建它们,上限是maxRecoverableRestarts,默认 3 次。)"not_ready"—— 仍在启动或收尾阶段。另外两种情况也会返回它:state为failed但进程还活着(单次调用的错误,如cwd覆盖被拒,下次调用即可恢复);进程刚死且被判为可恢复,下次提交会重建。
完整表格见 3.4 就绪分级。
"为什么上一次 turn 成功了,last_error 还在?"
这是有意设计。last_error / last_error_at 是粘性诊断字段——每分钟轮询一次的 host 仍然应该看到"最近一次失败是什么"。消费顺序:先看 state(或 readiness())决定是否放行,再把 last_error 当作历史诊断读。需要显式清空调 reset_last_error(name)。见 附录 D.5。
"last_error_at 是 raise 时刻吗?"
不是。它是 manager 存入错误的时刻,不是 raise 的时刻。用它判断"错误过没过期"(now - last_error_at > Δ)就够了,不要拿它做精确 raise 时刻取样。回归测试会 monkey-patch datetime 来断言时间戳确实来自存入时的 now(),而不是预先在别处算好的。
"为什么我的 "nonInteractivePolicy": "reject_all" 现在报 AcpConfigError?"
Week 3 把历史裸字符串形式下掉了。新形态是结构化对象:
"nonInteractivePolicy": { "mode": "reject_all" }错误是在配置加载阶段(AcpClientConfig.from_dict / load_acp_client_config)炸的,不会等到 send_prompt 再暴露——目的就是不让配置漂移悄悄上线。如果只是想单次调用覆盖,不用改配置,直接在 send_prompt / prompt_once 上传 interaction_policy=。完整迁移见 附录 E.7。
"server 中途挂了,怎么恢复?"
看怎么挂的(完整矩阵见 3.4 进程死亡分类):
- 可恢复死亡(干净退出、idle 非零退出且在重试上限内、stdio EOF、active turn 期间的非信号死亡):不需要人工干预。下一次
send_prompt/prompt_once会自动重建 client;mgr.restart_count(name)能看到目前自动重建了几次 - 致命死亡(被信号杀死——OOM / SIGKILL /
exit 137——/ 连续第二次 handshake 失败 / 超过maxRecoverableRestarts):server 被打上 sticky-fatal 标记。mgr.is_fatal(name)返回True,所有ensure_connected调用都会抛AcpClientError(code=TRANSPORT_DISCONNECT, details={"recovery": "fatal"})。运维需要显式调mgr.restart_server(name)或mgr.start_server(name)来确认并重新开启自动恢复
重试上限通过 server 配置中的 maxRecoverableRestarts 调(默认 3)。
"cancel_turn 之后再调 ensure_connected 安全吗?"
安全。清理保证(见 3.4 取消或超时后的清理顺序):
session/cancel发出去之前,pending slot 已经先 drop 掉了- turn slot 与 per-server lock 都在
finally里释放 last_error在锁释放之前就记录好,同 tick 的get_status()读者就能看到
下一次 send_prompt 看到的是一个 ready、没有残留 busy / locked 状态的 server。test_headless_runtime.py::TestDaemonRegression::test_cancel_then_continue 固化这个保证。
F.7 部署与运维
"Docker 镜像巨大"
多阶段构建——见 6.8 Dockerfile 模板。关键一步:别把 uv 带进运行时镜像。
"K8s pod 重启后会话丢了"
用 StatefulSet(不是 Deployment),/data 挂 PVC。Service 设 sessionAffinity: ClientIP。见 6.8。
"每租户怎么控 token 花销"
TokenBudget 模式——见 6.7。要精确计数装 agentao[tokenizer](拉 tiktoken)。
成本一夜翻倍
可能原因:
- 模型版本切换(查部署审计)
- 技能改了导致每轮调的工具变多
- 上下文压缩触发更频繁——查
max_context_tokens
对比昨天 vs 今天的 LLM_TEXT 事件 token 数。Session replay(6.6)让这件事可行。
F.8 开发与测试
"怎么单测自定义工具"
Tool 就是普通类——MyTool().execute(**args),不需要 Agentao 实例。要动磁盘的传临时 working_directory。
要端到端测试钻机——agent / agent_with_reply / fake_llm_client 三个 pytest fixture + 通过的烟雾测试——见 examples/pytest-fixture/。把 fixture drop 进自己测试套件,立刻获得无 OPENAI_API_KEY 依赖的密闭 Agentao 测试。
"怎么断言 agent 做对了事"
不要对 LLM 文本断言(不确定)。改为:
- 用
SdkTransport(on_event=spy)监听EventType.TOOL_START;断言工具以预期参数被调 - 或把工具 mock 了,断言交互
- 复用
examples/pytest-fixture/里的fake_llm_clientfixture,按轮次脚本化 LLM 回复
"测试时 LLM 响应每次不一样"
测试时设 temperature=0,但措辞还是会漂。断言效果(工具调用、最终文件、返回形状),不断言文字。
F.9 还是卡住?
最小复现 Bug 报告:
- Agentao 版本(
python -c "import agentao; print(agentao.__version__)") - OS、Python 版本
- ≤ 30 行可复现的脚本
- 失败前后的
agentao.log尾部 - ACP 问题:
AcpClientError.code+.details