Day 02 · 路径 A · 手把手

独立仓接入 · 每步改什么、怎么改

下文用仓库根 <repo>/my-agent/、Python 包 my_agent 举例。步骤 4 会把 demo「回声 bot」改成「三步摘要 bot」——你可以照抄结构,只换 prompt 和业务逻辑。

先认清目录(builder 不在 nodes/)

铁律builder.pymy_agent/builder.py,与 nodes/ 平级。nodes/ 里只有 run.py 等节点函数。
<repo>/my-agent/
├── pyproject.toml          ← 步骤 1、6
├── .env                    ← 步骤 3
├── tests/test_smoke.py     ← 步骤 7
└── my_agent/
    ├── builder.py          ← 步骤 4c(串图)
    ├── state.py              ← 步骤 4b
    ├── core.py               ← 步骤 5
    ├── cli.py                ← 一般不改
    ├── server.py             ← 步骤 6(pod)
    ├── nodes/run.py          ← 步骤 4a(业务逻辑)
    ├── nodes/__init__.py     ← 步骤 4c(多节点时)
    └── prompts/__init__.py   ← 步骤 4a(prompt)
1

起仓 + 全局改名

做什么
  1. 下载模板 zip,目录改名为 my-agent
  2. 把 Python 包目录 your_agent/ 改名为 my_agent/
  3. 全局替换占位符 your-agentmy-agentyour_agentmy_agent
curl -fL http://gov-agents-ui.bmaicsaws-prod.com/portal/v1/agent-template/download.zip -o /tmp/tpl.zip
unzip /tmp/tpl.zip && mv standalone-agent-template my-agent && cd my-agent
mv your_agent my_agent
grep -rl 'your[-_]agent' . | xargs sed -i '' 's/your-agent/my-agent/g; s/your_agent/my_agent/g'
git init && git add -A && git commit -m "init my-agent"

改文件:<repo>/my-agent/pyproject.toml

改完后应包含(核对 3 处):

name = "my-agent"
[project.scripts]
my-agent = "my_agent.cli:app"
[tool.setuptools.packages.find]
include = ["my_agent*"]
✅ 验证:grep -r your_agent . 无输出 · ls my_agent/builder.py 存在
2

配 Nexus 凭据

改文件:~/.netrc(用户主目录,不在项目里)

printf 'machine nexus.bitmartpro.com\nlogin pypi_pull\npassword 08gk5p2Q\n' >> ~/.netrc
chmod 600 ~/.netrc
✅ 验证:下一步 uv sync 不报 401
3

安装依赖 + LLM key + 首次跑通

改/新建:<repo>/my-agent/.env

怎么做
  1. 在仓库根执行 uv sync
  2. 复制 .env.example.env
  3. .env 填入 ANTHROPIC_API_KEY=sk-ant-...(或用 OpenAI 则配对应 key,见 pyproject 依赖)
uv sync
cp .env.example .env
# 编辑 .env,写入 ANTHROPIC_API_KEY=...
uv run my-agent run "你好"
✅ 期望:终端打印 success=True 和一句回复(无 key 时走 echo 兜底也算过)
4

写业务图(核心 · 4 个子步骤)

目标:把「原样回声用户输入」改成「对用户输入做三步摘要」。按顺序改下面 4 个文件。

4a · 改 prompt

<repo>/my-agent/my_agent/prompts/__init__.py

改前(模板默认):

RUN_SYSTEM = "你是一个有用的助手,简洁、准确地回答用户的问题。"

改后(你的业务 prompt):

RUN_SYSTEM = """你是发布说明助手。对用户给的内容输出三步摘要:
1. 背景(一句话)
2. 关键变更(最多 3 条 bullet)
3. 建议动作(一句话)
用中文,总字数不超过 200。"""

4b · 改 state(如需新字段)

<repo>/my-agent/my_agent/state.py

单节点 demo 可不改(继续用 input / output)。若多步节点之间要传中间结果,再加字段:

class AgentState(TypedDict, total=False):
    input: str
    output: str
    summary: str      # ← 例:scan 节点写入,run 节点读取

4c · 改节点业务逻辑

<repo>/my-agent/my_agent/nodes/run.py

改前:模板已是 LLM 调用,主要确认 import 和 prompt 引用

改后(保持结构,换 prompt + 可选解析):

from langchain_core.messages import HumanMessage, SystemMessage
from ai_trust_toolkit.llm import get_llm   # 必须走 toolkit,不要直连 anthropic SDK

from ..prompts import RUN_SYSTEM
from ..state import AgentState


def run_node(state: AgentState) -> AgentState:
    text = state.get("input", "")
    try:
        resp = get_llm("sonnet").invoke(
            [SystemMessage(content=RUN_SYSTEM), HumanMessage(content=text)]
        )
        out = getattr(resp, "content", str(resp))
        if isinstance(out, list):
            out = next((b.get("text", "") for b in out if isinstance(b, dict)), str(out))
    except Exception:
        out = f"(echo · 未接 LLM){text}"
    return {"output": out}   # 写回 state · builder 里注册的节点名仍是 "run"
注意:节点函数只返回要更新的字段 dict,不要 return 整个 state。

4d · 串图(单节点通常不用改)

<repo>/my-agent/my_agent/builder.py

仍在包根,不在 nodes/。单节点 demo 保持不动即可:

from .nodes import run_node
from .state import AgentState

def build_graph():
    from langgraph.graph import END, StateGraph
    g = StateGraph(AgentState)
    g.add_node("run", run_node)       # "run" 对应 nodes/run.py 的函数
    g.set_entry_point("run")
    g.add_edge("run", END)
    return g.compile()

若加第二节点(例 scan → summarize):

  1. 新建 my_agent/nodes/scan.py,写 def scan_node(state): ...
  2. my_agent/nodes/__init__.pyfrom .scan import scan_node
  3. builder.pyg.add_node("scan", scan_node)、改 entry 和 edge
✅ 验证:uv run my-agent run "本次发版改了登录和支付模块" → 输出应是三步摘要格式,不是简单回声
5

接 telemetry(Portal 看见 KPI)

<repo>/my-agent/my_agent/core.py

改两处
  1. 成功判定(约第 40 行):把 bool(final.get("output")) 换成你的标准,例如输出非空且不含 (echo
  2. 效果指标名(约第 61 行):把 record_effect("done", 1) 改成业务 key,如 record_effect("summarized", 1)
# core.py · run_agent 内
success = bool(final.get("output")) and "(echo" not in str(final.get("output", ""))

# core.py · _report 内
if result.success:
    record_effect("summarized", 1)   # ← Portal 上显示的领域效果名
✅ 验证:不带 --no-telemetry 跑一次 → 几分钟后 Portal /agents/my-agent 出现 invocation + summarized 计数
6

选入口形态(按需)

只 CLI:步骤 3 已有,不改文件

+ Pod HTTP

  1. <repo>/my-agent/pyproject.toml — dependencies 加一行:
    "fastapi>=0.110", "uvicorn>=0.30",
  2. <repo>/my-agent/my_agent/server.py — 底部启用 app:
    app = _make_app()   # 把原来的 app = None 改成这行
  3. 启动:uv run uvicorn my_agent.server:app --port 8080
  4. 调用:curl -X POST http://127.0.0.1:8080/agents/my-agent/run -H 'Content-Type: application/json' -d '{"input":"测试"}'

+ MCP:需另建 my-agent-mcp 包,参考平台 apps/bmc-agent-mcp(本教程不展开)。

7

补测试

<repo>/my-agent/tests/test_smoke.py

模板已有 2 条冒烟。可加一条断言输出格式:

def test_output_has_summary_structure():
    out = build_graph().invoke({"input": "发版说明:修复了 A 模块"})
    text = out.get("output", "")
    assert text
    # 有 LLM 时可断言包含序号;无 key echo 时跳过严格断言
    if "(echo" not in text:
        assert "1." in text or "背景" in text
uv run --extra test pytest -v   # 必须 --extra test
✅ 期望:全部 passed
8

安全自检 · 上线

步骤 8:若 agent 用服务凭据调 GitLab/工单/DB → 填 Day 05 安全 7 问(改方案文档,无固定代码文件)。本机 CLI + 用户自己的 key 通常可跳过正式评审。

步骤 9:在 <repo>/my-agent/ 根目录:

uv build
uv publish --publish-url http://nexus.bitmartpro.com/repository/bm-pypi/ \
  --username '<NEXUS_USER>' --password '<NEXUS_TOKEN>' dist/*

队友安装:pipx install --index-url http://nexus.bitmartpro.com/repository/local-pipy/simple/ my-agent

Monorepo 路径 → 同样业务改法,但 server 用 build_v1_router、另改 platform 注册文件 → Day 03 实操