Files
scaffold/template/.claude/rules/agent-llm-api.md
T
baozaotumao 39fe248f9f feat: 重组为标准 skill 包结构 + 修复 verify.sh bash 3.2 兼容
- 新增 .claude/skills/new-project/SKILL.md(标准 skill 格式)
- install-skill.sh 安装时组装 SKILL.md + verify.sh,单一源无重复
- verify.sh 改用索引数组替代 declare -A,兼容 macOS bash 3.2
- log_pass/log_skip 显式 return 0,避免 set -e 下非 verbose 误退出
- README 改为 skill 优先;补全 copier.yml/scripts/template 入库
2026-06-01 21:19:45 -07:00

111 lines
3.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
paths:
- "agent/**"
---
# Agent LLM 驱动层与 API 桥接
厂商无关的 LLM API 抽象、HTTP SDK 集成机制、超时保护与流式响应处理。
## 重要:LLMSession 端口是协议,不是机制
- `LLMSession` Protocol 只定义"能力"(发消息、收推送流、关闭),不规定底层是哪家 SDK
- 本文件描述 API SDK 适配器实现模式(OpenAI / Anthropic 等)
- PTY CLI 路径见 `agent-llm-pty.md`(当前配置未启用)
## LLMSession 协议(domain/ports.py
API SDK 场景使用**推送式流**`AsyncIterator`),而非拉取式 `stream_until`
```python
from typing import Protocol, AsyncIterator
class LLMSession(Protocol):
async def generate(
self,
messages: list[dict], # [{"role": "user", "content": "..."}]
) -> AsyncIterator[str]: ... # 逐 token 推送
async def close(self) -> None: ... # 释放 HTTP 连接池
```
## 适配器结构(llm/
| 文件 | 职责 |
|------|------|
| `domain/ports.py` | `LLMSession` 协议(端口,接口定义) |
| `llm/<provider>_api.py` | SDK 适配器实现(实现 `LLMSession` |
| `llm/registry.py` | 按 `Settings.llm_provider` 选择实现 |
`pty_bridge.py`,无 `skills/installer.py`——API 模式不需要 PTY 或 skill 安装。
## 适配器实现模式
```python
from typing import AsyncIterator, override
from domain.ports import LLMSession
class OpenAISession(LLMSession):
def __init__(self, settings) -> None:
from openai import AsyncOpenAI
self._client = AsyncOpenAI(
api_key=settings.llm_api_key,
base_url=settings.llm_base_url or None,
)
self._model = settings.llm_model
@override
async def generate(self, messages: list[dict]) -> AsyncIterator[str]:
stream = await self._client.chat.completions.create(
model=self._model,
messages=messages,
stream=True,
)
async for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
yield delta
@override
async def close(self) -> None:
await self._client.close()
```
Anthropic 适配器同理,将 `AsyncOpenAI` 换为 `AsyncAnthropic`,流式 API 换为 `client.messages.stream`
## DAG 节点中的调用模式
```python
async def phase3_generate(state, send, recv_queue):
try:
async with asyncio.timeout(settings.generate_timeout_seconds):
chunks: list[str] = []
async for token in llm_session.generate(state.messages):
chunks.append(token)
await send({"type": "llm_token", "text": token})
html = extract_html("".join(chunks))
if not html:
raise HtmlExtractionError()
except TimeoutError:
raise LLMTimeoutError(phase="generate")
```
不需要 ANSI 过滤——API 响应是纯文本,无终端转义码。
## 超时保护
每个 DAG 阶段用 `asyncio.timeout()` 包裹 LLM 调用:
| 阶段 | 配置项 |
|------|--------|
| phase1_dialog(单轮对话) | `dialog_timeout_seconds`(默认 60s |
| phase2_preview3 张预览) | `preview_timeout_seconds`(默认 180s |
| phase3_generate(完整 HTML | `generate_timeout_seconds`(默认 300s |
超时后抛 `LLMTimeoutError``domain/exceptions.py`),不静默挂起。
## 连接管理
`LLMSession` 实例在 DAG 节点开始时创建,在 `runner.py``finally` 块中通过 `await llm_session.close()` 确定性释放 HTTP 连接池,防止连接泄漏。
禁止在节点内手动调用 `close()`——由 runner 统一回收。