Files
agent/README.md
2025-09-01 11:09:34 +08:00

309 lines
11 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.
# Agent
自建基于 **llama-box** 启动 **Qwen3-Coder(-Flash)** 的 AI Agent 代码仓库。
本仓库新增 `langgraph_qwen` 轻量适配层,帮助你在 **LangGraph** 中直接接入 Qwen3含工具调用、流式增量、OpenAI 兼容接口)。并提供 **MCP**Model Context Protocol工具注入示例。
---
## 特性一览
- **LangGraph 适配**
- `get_qwen_chat_model`:创建 LangChain ChatModel优先 OpenAI 兼容路径,失败回退 DashScope
- `bind_qwen_tools`:将 LangChain 工具绑定到模型,支持 `tool_choice`
- 预置示例:`examples/qwen_langgraph_react.py``examples/qwen_langgraph_stream.py``examples/qwen_langgraph_custom_model.py`
- **自定义 ChatModel强烈推荐**
- `langgraph_qwen.ChatQwenOpenAICompat`
- 直接对接 OpenAI 兼容 **/v1/chat/completions**(可指向 DashScope 或自建网关/llama-box
- **非流式**:完整解析 `tool_calls`,装入 `AIMessage.tool_calls`,可被 LangGraph ToolNode 正确消费。
- **流式**:实现 SSE 增量组装器,安全缓冲 `delta.tool_calls[].function.name/arguments` 的碎片,在 `[DONE]` 时产出完整 `tool_calls`;文本 token 即时输出。
- **工具注入**`.bind_tools([...]).bind(tool_choice="auto")``extra_body` 透传服务端定制参数。
- **JSON Schema 兼容性增强**:内部使用 `convert_to_openai_tool` + 自定义归一化,避免网关对工具 schema 的严格校验导致 4xx/5xx。
- **工具名称校验**(默认启用):
- 仅允许 `[a-zA-Z0-9_-]`、长度 ≤ 64违规会报错并给出修复建议。
- 公共函数:`langgraph_qwen.validators.validate_tool_names` / `sanitize_tool_name`
- **MCP 工具注入**
- 通过官方库 **`langchain-mcp-adapters`** 获取 MCP 服务器工具,并注入 LangGraph。
- 示例:`examples/mcp_adapters/inject_to_langgraph.py`,包含 `streamable_http` 快速演示服务。
---
## 快速开始
### 1) 安装
```bash
uv pip install -U langgraph langchain httpx
# 可选:
uv pip install -U langchain-openai # 如需走 ChatOpenAI 生态
uv pip install -U '.[tongyi]' # 如需走 ChatTongyiDashScope SDK
uv pip install -U '.[mcp-adapters]' # 注入 MCP 工具
uv pip install -U '.[viz]' # 导出 Graph PNG 可视化
uv pip install -U '.[all]' # 一次装全(包含上面所有可选)
```
> 项目已组织为可安装包,建议在仓库根目录执行:
>
> ```bash
> uv pip install -e '.[openai,custom]'
> # 或uv pip install -e '.[openai]'
> # 或uv pip install -e '.[custom]'
> ```
### 2) 环境变量(必看)
> 适配器会自动读取 `.env`(已内置加载),也可直接 `export`。同名变量的优先级按示例说明。
**核心**
- `QWEN_BASE_URL`OpenAI 兼容基地址,如 `https://host/v1`;也可填完整端点 `.../v1/chat/completions`
- `QWEN_MODEL`:模型名,如 `qwen3-coder-flash-1M`(以后端服务可用名为准)。
- `QWEN_API_KEY`:客户端访问网关的 Key若网关启用鉴权必填
**兼容型 API Key择一**
- `QWEN_API_KEY`(优先)
- `GPUSTACK_API_KEY`(自建 gpustack 网关时可用)
- `OPENAI_API_KEY`OpenAI 风格兼容)
- `DASHSCOPE_API_KEY`(阿里云百炼 Key
**鉴权/网络/调试**
- `QWEN_AUTH_HEADER`:默认 `Authorization`;改为 `X-API-Key` 可适配自定义头。
- `QWEN_AUTH_SCHEME`:默认 `Bearer`;若网关只要裸 Key设为空字符串 `''`
- `QWEN_TIMEOUT`:请求超时秒数,默认 `60`,建议长上下文设大如 `180`
- `QWEN_HTTP_TRUST_ENV`:是否继承系统代理,默认 `1`;设 `0` 可禁用代理环境变量。
- `QWEN_DEBUG``1` 打印方法/URL/超时/代理选项。
- `QWEN_DEBUG_BODY``1` 打印请求 JSON自动打码密钥
- `QWEN_DEBUG_RESP``1` 打印响应/错误体。
**MCP示例工具用**
- `WEATHER_MCP_URL`MCP 工具服务地址,如 `http://127.0.0.1:8010/mcp`
- `WEATHER_TRANSPORT``streamable_http`HTTP 流)或 `stdio`
### 3) 最小示例ReAct + 工具调用)
```bash
export QWEN_BASE_URL='https://ai.jmsu.top/v1'
export QWEN_MODEL='qwen3-coder-flash-1M'
# 若你的网关启用了 Bearer 鉴权export QWEN_API_KEY='your_token'
# 推荐调试export QWEN_HTTP_TRUST_ENV=0 QWEN_DEBUG=1 QWEN_DEBUG_BODY=1 QWEN_DEBUG_RESP=1
python examples/qwen_langgraph_react.py
```
### 4) 流式示例SSE 增量)
```bash
python examples/qwen_langgraph_stream.py
```
> 实时输出 token在 `[DONE]` 时产出完整 `tool_calls`。
### 5) 自定义模型示例
```bash
python examples/qwen_langgraph_custom_model.py
```
---
## 自定义 ChatModel`ChatQwenOpenAICompat`
**最小用法**
```python
from langgraph_qwen import ChatQwenOpenAICompat
from langgraph.prebuilt import create_react_agent
from langchain_core.messages import HumanMessage
from langchain.tools import tool
@tool
def ping(_: str = "") -> str:
return "pong"
model = ChatQwenOpenAICompat(temperature=0).bind_tools([ping]).bind(tool_choice="auto")
agent = create_react_agent(model, [ping])
res = agent.invoke({"messages": [HumanMessage(content="调用工具 ping 并返回结果。")]})
print(res["messages"][-1].content)
```
**为什么用它**
- 避免第三方封装差异,统一工具/流式行为。
- 可靠的工具 schema 归一化(`convert_to_openai_tool` + 兜底修复)。
- 自定义鉴权头/鉴权前缀、超时、是否继承系统代理等。
---
## MCP将远程工具注入 LangGraph
### 启动一个最小 `streamable_http` MCP 服务
示例在 `examples/mcp_adapters/`,你也可以用 FastAPI/fastmcp 写个简易 weather 工具。
### 注入并调用
```bash
uv pip install -U '.[mcp-adapters]'
export WEATHER_MCP_URL='http://127.0.0.1:8010/mcp'
export WEATHER_TRANSPORT='streamable_http'
export QWEN_BASE_URL='https://ai.jmsu.top/v1'
export QWEN_MODEL='qwen3-coder-flash-1M'
# export QWEN_API_KEY='your_token' # 如网关要求鉴权
export QWEN_DEBUG=1
export QWEN_DEBUG_BODY=1
export QWEN_DEBUG_RESP=1
python examples/mcp_adapters/inject_to_langgraph.py
```
> 若看到 5xx多半是网关的模板/工具字段解析不兼容。适配器已做 schema 瘦身与修正;仍有问题可先把 `tool_choice="none"` 做两阶段调用(先思考,后仅注入目标工具)。
---
## 与网关/llama-box 的部署建议
- **直连 llama-box**:将 `QWEN_BASE_URL` 指向外部可达的 OpenAI 兼容端(如 Caddy 反代到 llama-box
- **Caddy 鉴权(推荐)**
-`/v1*` 反代前匹配 `Authorization: Bearer {env.API_TOKEN}`;未命中返回 `401`
- 反代配置应开启:`flush_interval -1``transport http { versions 1.1; keepalive 0; }`、移除 `Accept-Encoding`,避免 SSE 被缓冲或压缩。
- 客户端侧填 `QWEN_API_KEY=$API_TOKEN`。如用自定义头,设置 `QWEN_AUTH_HEADER`/`QWEN_AUTH_SCHEME`
- **避免多级模板改写**:某些中间层会对 `tools` 进行模板渲染或字段改写,导致 5xx直连或使用“透传”配置最稳。
---
## 常见问题FAQ
### 1) `StructuredTool does not support sync invocation.`
使用 `agent.ainvoke(...)` 或用 LangGraph 的异步入口;确保工具函数是异步或由运行时在工具节点异步执行。
### 2) 401 / `InvalidApiKey`
- 网关启用鉴权但客户端未传:设置 `QWEN_API_KEY`
- 使用了自定义头:配置 `QWEN_AUTH_HEADER` 和(必要时)置空 `QWEN_AUTH_SCHEME`
### 3) 502 / 500带 tools 时)
- 工具 schema 不被网关接受:使用本适配器(已做 JSON Schema 归一化),或先将 `tool_choice='none'` 做两阶段调用。
- 上游超时:调大 `QWEN_TIMEOUT`,检查上游模型负载。
- 中间层模板渲染异常:改为直连 llama-box 或关闭模板改写。
### 4) 请求莫名走系统代理
设置 `QWEN_HTTP_TRUST_ENV=0`;或在命令前写 `HTTPS_PROXY= HTTP_PROXY=`
### 5) 流式输出与工具参数碎片
`ChatQwenOpenAICompat` 负责缓冲与合并,无需额外处理;最终分块含完整 `tool_calls`
---
## 项目结构
```
.
├─ langgraph_qwen/
│ ├─ __init__.py
│ ├─ factory.py
│ ├─ chat_model.py
│ ├─ utils.py
│ └─ validators.py
├─ examples/
│ ├─ qwen_langgraph_react.py
│ ├─ qwen_langgraph_stream.py
│ ├─ qwen_langgraph_custom_model.py
│ ├─ mcp_adapters/
│ └─ stream_modes/
├─ server/ #(可选)演示用 weather MCP 服务
│ └─ weather_server.py
├─ pyproject.toml
├─ README.md
└─ qwen3_coder_with_qwen_agent.py
```
---
## `curl` 快速探针
**A带工具 + `tool_choice:auto`**
```bash
curl -i 'https://<host>/v1/chat/completions' \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $QWEN_API_KEY" \# 如无鉴权可省略
-d '{
"model":"qwen3-coder-flash-1M",
"messages":[{"role":"user","content":"纽约天气"}],
"tools":[{"type":"function","function":{
"name":"get_weather","description":"Get weather",
"parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}
}}],
"tool_choice":"auto"
}'
```
**B带工具 + `tool_choice:none`(只思考不调用)**
```bash
curl -i 'https://<host>/v1/chat/completions' \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $QWEN_API_KEY" \
-d '{
"model":"qwen3-coder-flash-1M",
"messages":[{"role":"user","content":"纽约天气"}],
"tools":[{"type":"function","function":{
"name":"get_weather","parameters":{"type":"object","properties":{"location":{"type":"string"}}}
}}],
"tool_choice":"none"
}'
```
**C最小化无工具**
```bash
curl -i 'https://<host>/v1/chat/completions' \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $QWEN_API_KEY" \
-d '{
"model":"qwen3-coder-flash-1M",
"messages":[{"role":"user","content":"你好"}]
}'
```
> 如需直连测试,临时禁用代理:`HTTPS_PROXY= HTTP_PROXY=`。
---
## llama-box 启动参考
> 对 Qwen3 的工具/推理友好:`--jinja`、`--enable-reasoning` 建议开启。
```bash
llama-box \
--host 0.0.0.0 \
--port 8080 \
--model /path/to/Qwen3-Coder-…-GGUF.gguf \
--chat-template chatml \
--jinja \
--enable-reasoning \
--flash-attn \
--cache-type-k q4_0 \
--cache-type-v q4_0 \
--ctx-size 262144 \
--gpu-layers 49 \
--threads 12 \
--threads-batch 16 \
--threads-http 16 \
--batch-size 1024 \
--ubatch-size 1024 \
--defrag-thold -1 \
--no-context-shift
```
---
## 贡献与许可证
欢迎提交 Issue/PR。许可证见仓库 `LICENSE`(如未提供,默认以仓库许可为准)。