)
1. 为什么我要把 Jira 操作交给 LangGraph 智能体日常开发里 Jira 是绕不开的但用起来是真的烦。创建一个 Story 要填一堆字段点 N 次下拉框需求文档散在 Confluence、API 说明在 Git、历史讨论在评论里查一个上下文要开五六个标签页任务状态还得人工手动更新经常和实际情况对不上。我试过用纯 Prompt 让大模型直接生成 Jira REST 请求结果字段名记错、JQL 拼错、状态流转 ID 对不上报错比手点还慢。后来我把这套流程拆成了三层LangGraph 做编排大脑RAG 做内部知识检索MCP Server 做 Jira 工具调用。LangGraph 的 StateGraph 能管理多步骤状态RAG 把 Confluence 文档和代码注释切片向量化后按需召回MCP Server 把 Jira 的增删改查封装成标准工具函数。这样智能体不需要记住 Jira REST API 的细节只需要决定“先查知识库还是先搜工单”然后调用对应工具就行。这篇文章面向的是已经会用 Python 写基本脚本、想把手头 Jira 操作自动化的开发者。你不需要事先精通 LangGraph 或 MCP 协议我会把可复制的节点配置、MCP Server 注册参数、TaoToken 统一 Key 接入示例都贴出来最后跑一次从 Jira 工单查询到评论回写的端到端验证。核心检索词就三个LangGraph 编排、RAG 检索增强、MCP Server 工具调用适合谁适合每天被 Jira 工单淹没、想用智能体把重复操作接管的研发和运维。整个链路的关键在于LangGraph 负责“想”RAG 负责“查”MCP Server 负责“做”。三者通过统一的模型接口串起来而模型接口我用 TaoToken 统一 Key 来接入省去在多个模型供应商之间切换的麻烦。下面从环境准备开始一步步把这条链路搭起来。2. TaoToken 统一 Key 接入与 MCP Server 前置准备在写 LangGraph 节点之前先把模型接入层和 Jira 工具层准备好。这一步不做完后面图跑起来会一直报 401 或工具找不到。2.1 获取 TaoToken 统一 Key 并配置环境变量TaoToken 的作用是提供一个统一的模型调用入口你不需要为每个模型单独申请 Key、单独改 Base URL。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后在控制台创建 API Key。API 端点统一用 https://taotoken.net/api 注意这个地址后面不加 UTM 参数。拿到 Key 之后不要硬编码在代码里用环境变量管理export TAOTOKEN_API_KEYsk-你的统一Key export TAOTOKEN_BASE_URLhttps://taotoken.net/api export JIRA_URLhttps://your-domain.atlassian.net export JIRA_EMAILyour-emailexample.com export JIRA_API_TOKEN你的JiraAPITokenJira 的 API Token 在 Atlassian 账户安全设置里生成和登录密码不是一回事。如果你用的是 Jira Server 而不是 Cloud认证方式可能是 Personal Access Token字段名要相应调整。2.2 安装依赖与 MCP Server 注册参数Python 侧需要安装 LangGraph、LangChain 的模型适配层、以及 MCP 客户端库pip install langgraph langchain-openai mcp jira python-dotenv这里用langchain-openai是因为 TaoToken 的接口兼容 OpenAI 格式把base_url指向 TaoToken 的 API 地址即可。MCP Server 我选择用官方 Python SDK 自己封装一个 Jira 工具服务注册参数如下# mcp_jira_server.py from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent import os from jira import JIRA app Server(jira-mcp) jira JIRA( serveros.environ[JIRA_URL], basic_auth(os.environ[JIRA_EMAIL], os.environ[JIRA_API_TOKEN]) ) app.list_tools() async def list_tools(): return [ Tool(namejira_search, description用 JQL 搜索工单, inputSchema{type: object, properties: {jql: {type: string}}, required: [jql]}), Tool(namejira_get_issue, description获取工单详情, inputSchema{type: object, properties: {issue_key: {type: string}}, required: [issue_key]}), Tool(namejira_add_comment, description给工单添加评论, inputSchema{type: object, properties: {issue_key: {type: string}, comment: {type: string}}, required: [issue_key, comment]}), ] app.call_tool() async def call_tool(name: str, arguments: dict): if name jira_search: issues jira.search_issues(arguments[jql], maxResults10) return [TextContent(typetext, text\n.join(f{i.key}: {i.fields.summary} for i in issues))] if name jira_get_issue: issue jira.issue(arguments[issue_key]) return [TextContent(typetext, textf{issue.key} | {issue.fields.summary} | {issue.fields.status.name})] if name jira_add_comment: jira.add_comment(arguments[issue_key], arguments[comment]) return [TextContent(typetext, textf评论已写入 {arguments[issue_key]})] async def main(): async with stdio_server() as (read, write): await app.run(read, write, app.create_initialization_options()) if __name__ __main__: import asyncio asyncio.run(main())这个 MCP Server 通过 stdio 传输LangGraph 侧用 MCP 客户端连接它。注册时三个关键参数传输方式选stdio启动命令是python mcp_jira_server.py环境变量把 Jira 认证信息传进去。如果你用 Cline 或 Claude Code 这类支持 MCP 的编辑器配置片段长这样{ mcpServers: { jira: { command: python, args: [/path/to/mcp_jira_server.py], env: { JIRA_URL: https://your-domain.atlassian.net, JIRA_EMAIL: your-emailexample.com, JIRA_API_TOKEN: 你的JiraAPIToken } } } }注意 Base URL、Key、Model ID 这三件套在模型侧也要对齐Base URL 用https://taotoken.net/apiKey 用TAOTOKEN_API_KEYModel ID 根据你选的模型填比如gpt-4o或claude-3-5-sonnet。三件套缺一个都会在调用时报错。3. LangGraph 节点配置与 RAGMCP 串联可复制片段这一节是核心。LangGraph 的 StateGraph 把整个流程拆成节点每个节点负责一件事路由、RAG 检索、MCP 工具调用、结果汇总。下面给出可直接复制的配置。3.1 定义 AgentState 与模型初始化先定义状态结构LangGraph 靠它在线程间传递数据# state.py from typing import TypedDict, Annotated, List from langgraph.graph.message import add_messages class AgentState(TypedDict): messages: Annotated[list, add_messages] jira_context: str rag_context: str next_action: str模型初始化指向 TaoToken# llm.py import os from langchain_openai import ChatOpenAI llm ChatOpenAI( modelgpt-4o, api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ[TAOTOKEN_BASE_URL], temperature0 )base_url末尾不要加/v1TaoToken 的 API 路径已经处理好了。如果你填成https://taotoken.net/api/v1会报 404。3.2 RAG 检索节点配置RAG 节点负责从向量库召回相关文档。这里假设你已经把 Confluence 文档切片并存入向量库用简单的内存检索做示例# rag_node.py from langchain_community.vectorstores import FAISS from langchain_openai import OpenAIEmbeddings embeddings OpenAIEmbeddings( api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ[TAOTOKEN_BASE_URL] ) vectorstore FAISS.load_local(faiss_index, embeddings, allow_dangerous_deserializationTrue) def rag_retrieve(state: AgentState): query state[messages][-1].content docs vectorstore.similarity_search(query, k3) context \n---\n.join(d.page_content for d in docs) return {rag_context: context}向量库的构建不在本文展开你可以用任何兼容 OpenAI Embedding 接口的模型只要把base_url指向 TaoToken 就行。3.3 MCP 工具调用节点与图编排MCP 客户端连接 Jira Server把工具列表转成 LangGraph 可调用的函数# graph.py import asyncio from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client from langgraph.graph import StateGraph, END from state import AgentState from llm import llm from rag_node import rag_retrieve server_params StdioServerParameters( commandpython, args[mcp_jira_server.py], env{JIRA_URL: os.environ[JIRA_URL], JIRA_EMAIL: os.environ[JIRA_EMAIL], JIRA_API_TOKEN: os.environ[JIRA_API_TOKEN]} ) async def call_mcp_tool(tool_name: str, arguments: dict): async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() result await session.call_tool(tool_name, arguments) return result.content[0].text def jira_agent_node(state: AgentState): prompt f你是 Jira 助手。根据以下上下文决定调用哪个工具。 RAG 上下文{state.get(rag_context, 无)} 用户请求{state[messages][-1].content} 可用工具jira_search, jira_get_issue, jira_add_comment 只输出 JSON{{tool: 工具名, args: {{...}}}} resp llm.invoke(prompt) import json decision json.loads(resp.content) result asyncio.run(call_mcp_tool(decision[tool], decision[args])) return {jira_context: result, messages: [{role: assistant, content: result}]} builder StateGraph(AgentState) builder.add_node(rag, rag_retrieve) builder.add_node(jira, jira_agent_node) builder.set_entry_point(rag) builder.add_edge(rag, jira) builder.add_edge(jira, END) graph builder.compile()这段代码里rag节点先召回知识jira节点再让模型决定调哪个 MCP 工具。实际生产环境你会加条件边和循环但作为最小可运行版本这个结构已经能跑通查询和回写。3.4 完整 settings 片段汇总把上面所有配置汇总成一个settings.json方便你直接复制{ model: { base_url: https://taotoken.net/api, api_key_env: TAOTOKEN_API_KEY, model_id: gpt-4o }, mcp_servers: { jira: { command: python, args: [mcp_jira_server.py], env: { JIRA_URL: https://your-domain.atlassian.net, JIRA_EMAIL: your-emailexample.com, JIRA_API_TOKEN: 你的JiraAPIToken } } }, rag: { index_path: faiss_index, top_k: 3 } }路径和字段名保持和代码里一致改的时候两边一起改否则会出现工具找不到或索引加载失败。4. 端到端验证从 Jira 工单查询到评论回写配置写完跑一次完整链路验证。我用的测试场景是查询某个项目下所有高优先级未关闭工单让智能体生成摘要并回写到指定工单评论里。4.1 发起查询请求# run_agent.py from graph import graph result graph.invoke({ messages: [{role: user, content: 帮我查一下 PROJ 项目下所有 High 优先级且未关闭的工单汇总后把摘要写到 PROJ-123 的评论里}] }) print(result[jira_context])执行后LangGraph 先走rag节点召回项目规范文档再走jira节点。模型解析出第一步应该调jira_searchJQL 大概是project PROJ AND priority High AND status ! ClosedMCP Server 收到请求后调用 Jira REST API返回工单列表。模型拿到列表后生成摘要再决定调jira_add_comment把摘要写入 PROJ-123。4.2 验证成功结果终端输出应该类似PROJ-101: 登录模块超时优化 PROJ-108: 支付回调重试逻辑 PROJ-115: 用户头像上传失败 评论已写入 PROJ-123去 Jira 页面打开 PROJ-123评论里应该出现智能体生成的摘要。如果评论没出现先检查jira_add_comment的权限Jira API Token 对应的账户必须对该工单有评论权限否则会静默失败或返回 403。4.3 验证 RAG 是否生效为了确认 RAG 真的参与了可以在请求里加一句“按照项目性能测试标准总结”。如果 RAG 正常摘要里会出现你向量库里存的性能标准描述如果 RAG 没生效摘要就是干巴巴的工单标题罗列。这一步能帮你区分是模型问题还是检索问题。整个链路跑通后你可以把run_agent.py包成一个 CLI 或 FastAPI 接口后续接 Slack 或飞书机器人就是加一层 webhook 的事。5. 本篇常见报错排查401、local proxy failed、reading choices、OAuth搭这套链路时我踩过的坑集中在四类报错逐个说清楚。5.1 401 Unauthorized最常见。分两种模型侧 401 和 Jira 侧 401。模型侧 401 通常是TAOTOKEN_API_KEY没读到或者base_url写错。检查方式echo $TAOTOKEN_API_KEY curl -H Authorization: Bearer $TAOTOKEN_API_KEY https://taotoken.net/api/models如果 curl 返回 401说明 Key 无效或过期去控制台重新生成。如果 curl 正常但 Python 报 401检查ChatOpenAI初始化时api_key是否真的传进去了有时候.env没加载会导致空字符串。Jira 侧 401 通常是 API Token 和邮箱不匹配或者用了登录密码而不是 API Token。Jira Cloud 必须用 API TokenJira Server 用 PAT。5.2 local proxy failed这个报错一般出现在 MCP 客户端连接 stdio server 时。原因是command或args路径不对子进程没启动起来。排查步骤先在终端手动跑python mcp_jira_server.py看能不能正常启动。如果手动能跑但 LangGraph 里报 local proxy failed检查StdioServerParameters里的args是不是绝对路径。相对路径在不同工作目录下会失效。另外如果你的环境里设置了HTTP_PROXY或HTTPS_PROXYMCP 子进程可能会尝试走代理导致连接失败。在env里显式清空env{JIRA_URL: ..., JIRA_EMAIL: ..., JIRA_API_TOKEN: ..., HTTP_PROXY: , HTTPS_PROXY: }5.3 reading choices 报错这个报错来自模型响应解析阶段通常是 TaoToken 返回的 JSON 结构和langchain-openai预期的不一致。常见原因是base_url多写了/v1或者模型 ID 填了一个不存在的名字。检查model字段是否和 TaoToken 控制台里列出的模型 ID 完全一致大小写敏感。还有一种情况是流式响应被中断choices数组为空。把streamingFalse显式设上或者检查网络是否稳定。5.4 OAuth 相关报错如果你用 Claude Code 或 Codex 这类工具接入可能会遇到 OAuth 报错。这类工具通常要求先完成 OAuth 授权流程再写auth.json。auth.json里要包含 Base URL、Key、Model ID 三件套{ base_url: https://taotoken.net/api, api_key: sk-你的统一Key, model: gpt-4o }如果 OAuth 流程卡住先确认回调地址没有被防火墙拦截再检查auth.json的路径是否和工具要求的一致。不同工具对auth.json的存放位置要求不同Claude Code 一般在~/.claude/auth.jsonCodex 在~/.codex/auth.json。5.5 工具调用返回空结果MCP 工具调用成功但返回空通常是 JQL 写错或权限不足。先用 Jira 网页版的 JQL 搜索框验证 JQL 语法确认能搜到结果再放到代码里。权限问题表现为返回 200 但结果为空检查 API Token 对应账户是否有 Browse Projects 权限。6. 把这条链路用起来接入方式与后续扩展这套 LangGraph RAG MCP Server 的结构跑通后扩展方向很明确。想操作 Confluence再加一个 MCP Server 暴露 Confluence 工具想接 Jenkins同样封装成 MCP 工具注册进去。LangGraph 的 StateGraph 只需要加节点和边路由逻辑复用现有的条件判断。如果你只是想先验证模型调用是否正常可以直接用模型对话页面发一条测试消息确认 Key 和 Base URL 没问题。地址是 https://taotoken.net/api-keys 先创建 Key再去 https://taotoken.net/doc 看接入文档里的完整参数说明。长期跑编码类 Agent 或需要频繁调用模型的场景可以了解 Coding Plan它适合需要稳定模型配额和统一管理的开发者。控制台在 https://taotoken.net/console API Key 管理在 https://taotoken.net/api-keys 文档在 https://taotoken.net/doc 。Claude Code 和 Anthropic 相关接入参考 https://taotoken.net/claude-code-anthropic 。最后说一个实用技巧MCP Server 的工具描述字段description写得越具体模型选错工具的概率越低。比如jira_search的描述里加上“用 JQL 语法不要传自然语言”能减少模型把整句话当 JQL 传进去的情况。这个细节在调试阶段能省不少时间。