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

390 lines
13 KiB
Markdown
Raw Permalink 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` 快速演示服务。
---
## 待完成
- [ ] 阿里云百炼 Key 的测试兼容
- [ ] FASTMCP 兼容适配MCPHUB适配server目录下的已经测试案例适配
## 快速开始
### 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
```
## 使用 MCPFastMCPHTTP streamable注入工具到 LangGraph
通过官方库 langchain-mcp-adapters可以把任意 MCP 服务器暴露的工具注入到 LangGraph 的 ReAct 代理里使用。我们提供了开箱示例与一份更工程化的封装建议。
### 安装依赖
```bash
uv pip install -e '.[mcp-adapters]' # 本仓库 extras等价安装 langchain-mcp-adapters
# 最少还需要 fastapi/uvicorn若用我们内置的演示服务
uv pip install fastapi uvicorn
```
> 若你只需要客户端能力(连接已有 MCP 服务器),安装 langchain-mcp-adapters 即可;若要本地起一个演示工具服务,再装 fastapi/uvicorn。
### 启动测试mcp服务器
```bash
python examples/mcp_adapters/server/weather_server.py
```
该服务注册了一个异步工具:
get_weather(location: str) -> str返回“某地晴”的示例文案
想换你自己的工具?改这个文件里用 @mcp.tool() 注册即可。
### 执行测试案例
```bash
# 强烈建议避免本地回环被代理: NO_PROXY=localhost,127.0.0.1
export NO_PROXY=localhost,127.0.0.1,127.0.0.1:8000
# 指向你刚刚起的 MCP 服务地址(注意尾斜杠)
export WEATHER_MCP_URL='http://127.0.0.1:8000/mcp'
export WEATHER_TRANSPORT='streamable_http'
# QwenOpenAI兼容后端必需
export QWEN_BASE_URL='https://your-gateway-or-llamabox-host/v1' # 或完整 /v1/chat/completions
export QWEN_MODEL='qwen3-coder-flash-1M'
# 若你的后端需要鉴权,额外配置:
# export QWEN_API_KEY='...' # 或 OPENAI_API_KEY / DASHSCOPE_API_KEY
# export QWEN_AUTH_HEADER='Authorization' # 默认即可
# export QWEN_AUTH_SCHEME='Bearer' # 裸 key 时置空: ''
# 调试(可选)
export QWEN_DEBUG=1
export QWEN_DEBUG_BODY=1
export QWEN_DEBUG_RESP=1
python examples/mcp_adapters/inject_to_langgraph.py
```
预期输出:
首行打印 Discovered tools: get_weather
模型首轮会“列出工具并选择一个调用”,随后 LangGraph ToolNode 会执行 MCP 工具,并给出中文总结
为什么 URL 需要尾斜杠?
我们演示服务注册的是 POST /mcp/,不少 HTTP 路由器对 /mcp 与 /mcp/ 区分严格,建议总是使用尾斜杠或在服务端做 301/307 兼容。
### 封装使用
新建 `langgraph_qwen/mcp.py`, `langgraph_qwen/__main__.py`
```bash
# 方式一:用环境变量
export WEATHER_MCP_URL='http://127.0.0.1:8000/mcp/'
qwen-mcp-agent --prompt '在北京查天气并总结'
# 方式二:传入多服务器 JSON
qwen-mcp-agent --servers-json '{
"weather":{"url":"http://127.0.0.1:8000/mcp/","transport":"streamable_http"},
"calc":{"url":"http://127.0.0.1:8011/mcp/","transport":"streamable_http"}
}'
```
---
## 自定义 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`(如未提供,默认以仓库许可为准)。