From 0cd43c37a75d2cdf4cc704434e95e909a8464318 Mon Sep 17 00:00:00 2001 From: dark Date: Tue, 2 Jun 2026 14:06:22 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81api=5Fkey=E5=8F=AF=E4=BB=A5=E4=B8=BA?= =?UTF-8?q?=E7=A9=BA=202=E3=80=81=E7=8E=AF=E5=A2=83=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E8=A1=A5=E5=85=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 32 +++++++++++++++--- pam_deploy_graph/llm/factory.py | 2 -- pam_deploy_graph/llm/openai_compatible.py | 10 +++--- tests/test_llm_structured.py | 40 +++++++++++++++++++++++ 4 files changed, 72 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 2dcd13e..c1c8ee3 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ packaging/ - 实现人工确认入口:`confirm --decision approve|reject` 只处理待确认回滚。 - 实现 checkpoint 自动保存和 `resume` 续跑:全局步骤、成功 IP、单 IP 已完成 action 会跳过。 - 实现 LLM structured output 骨架:意图识别、参数抽取、部署计划生成。 -- 实现 OpenAI-compatible 真实 LLM client,支持 `base_url` / `api_key` / `model` 配置。 +- 实现 OpenAI-compatible 真实 LLM client,支持 `base_url` / `model` 配置,`api_key` 可为空。 - 固化真实 LLM 提示词:意图识别、参数抽取、部署计划生成均要求 JSON structured output。 - 增加规则 fallback `RuleBasedLlmClient`,用于本地开发和测试。 - 增加 LLM 输出 guardrails,禁止计划中出现可执行脚本命令和非法 action。 @@ -90,11 +90,12 @@ packaging/ ## LLM 配置 -默认不配置 LLM 时,`analyze` 使用本地规则 fallback。配置真实 LLM 后,会走 OpenAI-compatible `/chat/completions`: +默认不配置 LLM 时,`analyze` 使用本地规则 fallback。配置真实 LLM 后,会走 OpenAI-compatible `/chat/completions`。 + +`base_url` 和 `model` 必填;`api_key` 可为空。如果模型服务不需要鉴权,不配置 `PAM_LLM_API_KEY` 或不传 `--llm-api-key` 即可,Agent 不会发送 `Authorization` 请求头。 ```powershell $env:PAM_LLM_BASE_URL="https://your-llm.example.com/v1" -$env:PAM_LLM_API_KEY="your-api-key" $env:PAM_LLM_MODEL="your-model-name" python -m pam_deploy_graph.cli analyze --config doc_scripts/config.txt.example --text "请用 MCP 预演部署 HET PAM Node 版本 2.0.5,不要动环境" @@ -107,12 +108,17 @@ python -m pam_deploy_graph.cli analyze \ --config doc_scripts/config.txt.example \ --text "请用 MCP 预演部署 HET PAM Node 版本 2.0.5,不要动环境" \ --llm-base-url https://your-llm.example.com/v1 \ - --llm-api-key your-api-key \ --llm-model your-model-name ``` 真实 LLM 调用位置在 `pam_deploy_graph/llm/openai_compatible.py`,提示词在 `pam_deploy_graph/llm/prompts.py`。发送给 LLM 的 `base_params` 会脱敏,`CLIENT_SECRET` 不会进入 prompt;本地生成计划后仍会执行 guardrails 校验。 +如果服务需要鉴权,再补充: + +```bash +export PAM_LLM_API_KEY="your-api-key" +``` + ## MCP Client 配置 CLI/chat 已支持通过 `--mcp-config` 直接加载 MCP 配置。常用场景只需要配置 MCP `server_url` 和独立鉴权信息;Agent 会连接 MCP server,调用 `list_tools` 自动发现 server 暴露的 tools,再按 action 名自动匹配。 @@ -183,6 +189,24 @@ agent = PamDeployAgent(mcp_runner=runner) ## 使用方式 +开发项目迁移到新环境后,推荐先安装完整开发依赖: + +```bash +python -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip setuptools wheel +python -m pip install -e ".[mcp,chat,test]" +pytest -q +``` + +Windows PowerShell 激活虚拟环境使用: + +```powershell +.\.venv\Scripts\Activate.ps1 +``` + +基础要求:Python 3.11+。如果要执行 Linux 脚本 action,运行环境还需要 `bash` 和 `curl`;如果要构建 Linux 解压即用包,请在 Linux x86_64 构建机上执行打包脚本。 + 整体逻辑结构流程图: ```text diff --git a/pam_deploy_graph/llm/factory.py b/pam_deploy_graph/llm/factory.py index a4e2c6b..189fc9b 100644 --- a/pam_deploy_graph/llm/factory.py +++ b/pam_deploy_graph/llm/factory.py @@ -26,8 +26,6 @@ def build_llm_client( missing = [] if not actual_base_url: missing.append("base_url") - if not actual_api_key: - missing.append("api_key") if not actual_model: missing.append("model") if missing: diff --git a/pam_deploy_graph/llm/openai_compatible.py b/pam_deploy_graph/llm/openai_compatible.py index c5b515a..76e6ed2 100644 --- a/pam_deploy_graph/llm/openai_compatible.py +++ b/pam_deploy_graph/llm/openai_compatible.py @@ -43,8 +43,6 @@ class OpenAICompatibleLlmClient: """保存连接参数、模型参数和可替换的 HTTP transport。""" if not base_url: raise ValueError("必须配置 LLM base_url") - if not api_key: - raise ValueError("必须配置 LLM api_key") if not model: raise ValueError("必须配置 LLM model") self.base_url = base_url.rstrip("/") @@ -178,12 +176,12 @@ class OpenAICompatibleLlmClient: }, ], } + headers = {"Content-Type": "application/json"} + if self.api_key: + headers["Authorization"] = f"Bearer {self.api_key}" response = self.transport( _chat_completions_url(self.base_url), - { - "Authorization": f"Bearer {self.api_key}", - "Content-Type": "application/json", - }, + headers, request_payload, self.timeout_sec, ) diff --git a/tests/test_llm_structured.py b/tests/test_llm_structured.py index 31193b2..bf72678 100644 --- a/tests/test_llm_structured.py +++ b/tests/test_llm_structured.py @@ -2,6 +2,7 @@ from dataclasses import asdict from pam_deploy_graph.agent import PamDeployAgent from pam_deploy_graph.checkpoint_store import redact_mapping +from pam_deploy_graph.llm.factory import build_llm_client from pam_deploy_graph.llm.openai_compatible import OpenAICompatibleLlmClient from pam_deploy_graph.llm.rule_based import RuleBasedLlmClient from pam_deploy_graph.llm.validators import validate_deploy_plan @@ -110,6 +111,45 @@ def test_openai_compatible_client_uses_base_url_api_key_and_prompt(): assert "只输出一个 JSON 对象" in calls[0][2]["messages"][0]["content"] +def test_openai_compatible_client_allows_empty_api_key(): + calls = [] + + def transport(url, headers, payload, timeout_sec): + calls.append((url, headers, payload, timeout_sec)) + return { + "choices": [ + { + "message": { + "content": ( + '{"intent":"deploy","mode_preference":"未指定",' + '"strategy_preference":"fake","confidence":0.8}' + ) + } + } + ] + } + + client = OpenAICompatibleLlmClient( + base_url="https://llm.example/v1", + api_key="", + model="model-a", + transport=transport, + ) + + result = client.understand_request("部署") + + assert result.intent == "deploy" + assert "Authorization" not in calls[0][1] + assert calls[0][1]["Content-Type"] == "application/json" + + +def test_llm_factory_allows_empty_api_key_with_base_url_and_model(): + client = build_llm_client(base_url="https://llm.example/v1", model="model-a") + + assert isinstance(client, OpenAICompatibleLlmClient) + assert client.api_key == "" + + def test_openai_compatible_client_does_not_send_base_secret(): calls = []