- 人工智能
- AI Agent
- Agent 框架
- 大模型
- 工具调用
- RAG
- 提示工程
- 强化学习
【免费下载链接】agent-core
openJiuwen agent-core可提供AI Agent开发、运行、调优与演进相关的全套SDK能力
本文是一份面向 AI Agent 开发者的技术实战指南,围绕 openJiuwen agent-core 中的LspRail与 Language Server Protocol(LSP)集成展开:只需一行rails=[LspRail()],即可让DeepAgent获得lsp工具——跳转定义、查找引用、列出文件符号、全局符号搜索、调用层级分析,并在编辑文件后自动重分析、自动把 pyright 等语言服务器的诊断注入下一轮 LLM 上下文。读完本文,你将掌握 LSP 子系统的完整生命周期、lsp工具的全部操作与参数语义、自动诊断注入流水线的原理,以及如何通过InitializeOptions/CustomServerConfig定制语言服务器。
前置条件
在运行任何示例前,需要先安装目标语言对应的语言服务器,并配置 LLM 运行所需的环境变量。
以 Python 的 pyright 为例,可用以下任一方式安装:
# npm(推荐) npm install -g pyright # pip pip install pyright # pipx(隔离安装) pipx install pyright设置 LLM 所需的环境变量:
export API_KEY=... export API_BASE=https://api.openai.com/v1 export MODEL_NAME=gpt-5.2 export MODEL_PROVIDER=OpenAI说明:仓库内示例(如 examples/lsp/deep_agent_lsp_demo.py)默认从
API_KEY、API_BASE、MODEL_NAME三个环境变量读取模型配置,未设置时使用占位字符串。
完整示例:9 个 Demo 一次跑通
仓库提供了可运行的全流程端到端示例 examples/lsp/deep_agent_lsp_demo.py,覆盖 8 个 LSP 代码导航操作和 1 个自动诊断注入闭环:
| Demo | 操作 | 说明 |
|---|---|---|
| 1 | goToDefinition | 跳转到函数的定义 |
| 2 | findReferences | 查找符号的所有使用位置 |
| 3 | documentSymbol | 列出文件内全部符号 |
| 4 | workspaceSymbol | 跨整个项目搜索符号 |
| 5 | goToImplementation | 查找抽象方法的具体实现 |
| 6 | prepareCallHierarchy | 准备调用层级条目 |
| 7 | incomingCalls | 查找调用某函数的所有调用方 |
| 8 | outgoingCalls | 查找某函数调用的所有下游函数 |
| 9 | before_model_call诊断注入 | edit_file→ 自动触发 LSP → 自动注入诊断 → Agent 修复全部错误 |
运行方式(需已安装 pyright 并配置好 API 环境变量):
uv run python examples/lsp/deep_agent_lsp_demo.py示例以examples/lsp/sample_code/为分析对象,其中 test.py 故意包含类型错误(如x: int = "not_an_integer"),用于 Demo 9 验证 Agent 的"读取 → 编辑 → 诊断 → 修复"闭环。示例还通过 diagnostic_params.py 提供的wait_for_diagnostics在 Demo 9 结束后等待 pyright 最终诊断,确认诊断队列为空才算通过。
工作原理:LspRail 如何接入 Agent 生命周期
LspRail是DeepAgentRail的一个具体实现(源码见 openjiuwen/harness/rails/lsp_rail.py),其priority = 60(中高优先级)。它将 LSP 子系统"接线"进 Agent 的四个生命周期事件:
| 生命周期事件 | LspRail的行为 |
|---|---|
init()— Agent 启动 | 解析 LSP 选项,并把LspTool注册到 Agent 的ability_manager上 |
| Agent 运行中 | LLM 可自由调用lsp工具;语言服务器在首次请求时才懒加载启动;publishDiagnostics通知被缓冲进LspDiagnosticRegistry |
after_tool_call—edit_file/write_file之后 | 向语言服务器发送textDocument/didChange触发重新分析;新诊断异步(fire-and-forget)缓冲 |
before_model_call— 每次 LLM 调用前 | 异步初始化LSPServerManager(首次调用前),随后排空缓冲诊断并作为UserMessage注入,LLM 无需显式调用诊断工具即可看到错误 |
uninit()— Agent 停止 | 从ability_manager移除LspTool,关闭所有语言服务器进程 |
从源码看,init()中会做以下几件关键事(lsp_rail.py):
- 类型守卫:仅当 agent 是
DeepAgent且存在deep_config、ability_manager时才生效,否则仅记录 warning 并跳过; - cwd 解析优先级:
options.cwd>workspace.root_path,最终写入InitializeOptions; - verbose 日志文件:若
verbose=True,会在仓库根目录的logs/logs/lsp/下创建lsp_YYYYMMDD_HHMMSS.log(UTC 时间戳,每次运行新建); - 异步初始化:
_start_lsp_initialization()通过loop.create_task启动_ensure_lsp_initialized(),内含 15 秒超时保护,避免阻塞主事件循环。
LspRail()无参数时,自动继承 Agent 的workspace路径并使用默认语言服务器设置。Agent 侧只看到一个名为lsp的工具,其描述与完整操作 Schema 会通过工具元数据注册表自动注入系统提示词,无需手动编写。
快速开始:给 DeepAgent 挂载 LSP
最小可运行示例(LspRail()无参数即可继承 workspace):
import asyncio import os from openjiuwen.core.foundation.llm import init_model from openjiuwen.core.single_agent.schema.agent_card import AgentCard from openjiuwen.core.runner import Runner from openjiuwen.harness.factory import create_deep_agent from openjiuwen.harness.rails.lsp_rail import LspRail from openjiuwen.harness.lsp import shutdown_lsp _API_KEY = os.getenv("API_KEY", "your api key here") _MODEL_NAME = os.getenv("MODEL_NAME", "your model here") _API_BASE = os.getenv("API_BASE", "your api base here") async def main(): await Runner.start() model = init_model( provider="OpenAI", model_name=_MODEL_NAME, api_key=_API_KEY, api_base=_API_BASE, ) agent = create_deep_agent( model=model, card=AgentCard( name="code_navigator", description="Navigates source code via LSP.", ), system_prompt="You are a code-navigation assistant. Use the lsp tool to answer questions.", rails=[LspRail()], # <-- one line to add LSP workspace="/path/to/repo", ) try: result = await Runner.run_agent( agent, {"query": "What classes are defined in src/models.py?"}, ) print(result) finally: await shutdown_lsp() await Runner.stop() asyncio.run(main())两点注意:
- 记得在
finally中调用await shutdown_lsp()关闭所有语言服务器进程(对应LspRail.uninit()中的清理逻辑,含 10 秒超时保护,见 lsp_rail.py); shutdown_lsp与initialize_lsp、get_pending_lsp_diagnostics、get_lsp_status等都在 openjiuwen/harness/lsp/init.py 中作为公共 API 导出。
开启自动诊断注入
仅挂LspRail只能获得代码导航能力;要激活"编辑文件 → 自动重分析 → 自动注入诊断"的闭环,需要同时挂载SysOperationRail(它提供edit_file/write_file工具):
from openjiuwen.harness.rails.sys_operation_rail import SysOperationRail from openjiuwen.harness.rails.lsp_rail import LspRail from openjiuwen.harness.lsp import InitializeOptions rails = [ SysOperationRail(), # provides edit_file / write_file LspRail(options=InitializeOptions(cwd="/path/to/repo"), verbose=True), # LSP + auto-inject ]两个 rail 同时生效后,每次edit_file/write_file调用都会自动触发语言服务器重新分析,产生的诊断自动注入下一轮 LLM 上下文——Agent 全程无需显式调用任何诊断工具。
自动诊断注入流水线深度解析
LspRail通过两个互补的生命周期钩子,构成编辑后的连续反馈回路。
after_tool_call— 触发重分析
每次 Agent 调用edit_file或write_file后,LspRail.after_tool_call自动触发(lsp_rail.py):
- 从
ctx.inputs读取tool_name,只有当其命中内部集合_WRITE_TOOL_NAMES = {"edit_file", "write_file"}时才继续; - 解析编辑文件的绝对路径——相对路径按
workspace.root_path或当前工作目录解析(在调用线程上提前 resolve,避免协程内 cwd 变化导致路径失效); - 按扩展名推断
language_id(.py/.pyi映射为"python",其余取去掉点号的扩展名); - 若文件从未打开过,先发
textDocument/didOpen再发textDocument/didChange——因为部分服务器(如 pyright)要求先didOpen才接受didChange; - 立即返回,重新分析为 fire-and-forget:pyright 异步运行并发布
publishDiagnostics通知,由LspDiagnosticRegistry缓冲。
无需任何配置——只要挂载LspRail,该钩子即默认生效。
before_model_call— 把诊断注入上下文
每次 LLM 调用前,LspRail.before_model_call会调用get_pending_lsp_diagnostics()排空诊断注册表;若有待交付诊断,则格式化为文本并 append 一条UserMessage到消息列表(lsp_rail.py)。注入消息形如:
[LSP Diagnostics] The following issues were detected after the last file edit: File: src/models.py [Error] line 12, col 15 (reportArgumentType) Argument of type "str" cannot be assigned to parameter "age" of type "int" Please review and fix these issues.格式化逻辑由_format_diagnostics实现:严重级别映射为1=Error, 2=Warning, 3=Info, 4=Hint,行/列号由 LSP 的 0-indexed 转回 1-indexed,并附带诊断 code。Agent 看到错误后直接修复,无需显式调用诊断工具;循环持续直到诊断队列为空。
Verbose 日志
给LspRail传verbose=True,每次before_model_call的诊断快照都会追加写入带时间戳的日志文件:
LspRail(verbose=True)日志路径为仓库根目录下的logs/logs/lsp/lsp_YYYYMMDD_HHMMSS.log(每次运行新建,UTC 时间)。内容包含[before_model_call] server=... local_path=...头以及每条诊断的[级别] line x, col y (code) 消息,非常适合离线调试多轮修复循环。
lsp工具:10 个操作完整参考
LspRail激活后,LLM 可调用lsp工具执行下述操作。所有位置参数line、character均为1-indexed(与编辑器显示一致);工具会在发送给语言服务器前转换为 0-indexed(见 openjiuwen/harness/tools/lsp_tool/_schemas.py 中ge=1的字段约束,以及 _tool.py 中validated.line - 1的转换逻辑)。
导航操作
| 操作 | LSP 方法 |
|---|---|
documentSymbol | textDocument/documentSymbol |
goToDefinition | textDocument/definition |
findReferences | textDocument/references |
workspaceSymbol | workspace/symbol |
goToImplementation | textDocument/implementation |
prepareCallHierarchy | textDocument/prepareCallHierarchy |
incomingCalls | callHierarchy/incomingCalls |
outgoingCalls | callHierarchy/outgoingCalls |
诊断操作
| 操作 | 用途 |
|---|---|
changeFile | 发送textDocument/didChange通知服务器新内容,触发重新分析 |
getDiagnostics | 排空缓冲的publishDiagnostics通知,返回格式化后的错误/警告 |
操作到 LSP 方法的映射集中在_operation_to_method()(openjiuwen/harness/tools/lsp_tool/_tool.py)。操作定义使用 Pydantic discriminated union 校验(LspOperation枚举),非法输入会直接返回Invalid input错误。
documentSymbol— 列出文件内全部符号
返回单个文件中定义的所有类、函数、方法、变量。无需位置参数。
{ "operation": "documentSymbol", "file_path": "src/models.py" }goToDefinition— 跳转符号定义
{ "operation": "goToDefinition", "file_path": "src/app.py", "line": 42, "character": 15 }findReferences— 查找符号全部引用
{ "operation": "findReferences", "file_path": "src/models.py", "line": 10, "character": 7, "include_declaration": True # include the definition site in results (default: True) }该操作会把includeDeclaration写进 LSP 请求的context字段(见_build_lsp_params)。
workspaceSymbol— 全项目搜索符号
{ "operation": "workspaceSymbol", "file_path": "", # can be empty for workspace-wide search "query": "UserRepository" }goToImplementation— 查找抽象方法实现
{ "operation": "goToImplementation", "file_path": "src/base.py", "line": 20, "character": 9 }注意:并非所有语言服务器都实现此操作,例如 pyright 不支持
textDocument/implementation。工具对"不支持的操作"会返回明确的错误信息(识别Unhandled method或-32601错误码),而不是静默失败(见 openjiuwen/harness/tools/lsp_tool/_tool.py)。
prepareCallHierarchy— 解析符号为调用层级条目
{ "operation": "prepareCallHierarchy", "file_path": "src/services.py", "line": 55, "character": 5 }incomingCalls— 查找函数的所有调用方
工具会自动先执行prepareCallHierarchy,因此只需提供位置:
{ "operation": "incomingCalls", "file_path": "src/services.py", "line": 55, "character": 5 }outgoingCalls— 查找函数调用的下游函数
{ "operation": "outgoingCalls", "file_path": "src/services.py", "line": 55, "character": 5 }底层实现:
incomingCalls/outgoingCalls需要 LSP 请求中的item字段是CallHierarchyItem而非textDocument + position。因此call_lsp_tool会先发textDocument/prepareCallHierarchy拿到条目,再把参数替换为{"item": call_item}后发callHierarchy/incomingCalls/callHierarchy/outgoingCalls(见 openjiuwen/harness/tools/lsp_tool/_tool.py)。
changeFile— 通知服务器文件内容变化
发送textDocument/didOpen(若文件尚未打开)后跟textDocument/didChange。语言服务器重新分析新内容并发布publishDiagnostics通知,缓冲供下一次getDiagnostics使用。
{ "operation": "changeFile", "file_path": "src/models.py", "content": "class User:\n name: str\n age: int\n" # full file text }content必须是文件的完整新文本(full-sync 模式,不做 diff)。这与LSPServerManager.change_file的实现一致——contentChanges仅含{"text": content}、无range字段(见 openjiuwen/harness/lsp/core/manager.py);文件未预先打开时版本号从 1 开始,每次didChange递增。
getDiagnostics— 取回缓冲诊断
排空自上次调用以来所有待交付的publishDiagnostics通知,返回按严重级别排序的格式化列表。每次调用还会跨批次去重,避免同一条错误重复出现。
# All files with pending diagnostics { "operation": "getDiagnostics", "file_path": "" } # Filtered to a single file { "operation": "getDiagnostics", "file_path": "src/models.py" }诊断工作流:显式与自动两种模式
显式工作流(通过lsp工具)
典型的显式检查模式:
changeFile → (wait for server re-analysis) → getDiagnostics示例 Agent 提示词:
result = await Runner.run_agent( agent, { "query": ( "Change src/models.py so that the `age` field is typed as `str` instead of `int`, " "then get the diagnostics to see if pyright reports any type errors." ) }, )Agent 将依次调用:
lsp(operation="changeFile", file_path="src/models.py", content="...")— 把新内容发给 pyright;lsp(operation="getDiagnostics", file_path="src/models.py")— 取回类型错误。
自动工作流(after_tool_call+before_model_call)
同时挂载SysOperationRail后,Agent 用edit_file编辑文件即可自动收到诊断反馈,无需调用changeFile/getDiagnostics:
result = await Runner.run_agent( agent, { "query": ( "Read src/models.py and fix all type errors. " "Keep editing until no errors remain." ) }, )每次edit_file触发after_tool_call,向 pyright 发送textDocument/didChange;下一轮 LLM 调用前,before_model_call把新诊断作为UserMessage注入。Agent 看到错误后继续修复,直至诊断队列清空。
上限与去重
| 配置 | 默认值 |
|---|---|
| 单文件最大诊断数(Max diagnostics per file) | 10 |
| 诊断总数上限(Max diagnostics total) | 30 |
诊断在应用上限前按严重级别排序(Error → Warning → Info → Hint)。跨调用去重会抑制上一轮已交付过的条目。
这些语义在 openjiuwen/harness/lsp/core/diagnostic_registry.py 中完整实现:LspDiagnosticRegistry是进程级单例,内部维护_pending(UUID → 通知批次)与_delivered(uri → 已交付 key 集合)两个结构。get_and_clear()依次执行:按 URI 合并批次 → 批内去重(key 由message|severity|line:char|code构成)→ 跨轮去重 → 严重级别升序排序(Error=1 在前)→ 单文件截断 → 全局截断 → 记录已交付历史。由于通知回调与get_and_clear都在 asyncio 事件循环线程执行,无需额外加锁。
自定义语言服务器:InitializeOptions 与 CustomServerConfig
通过InitializeOptions可覆盖默认服务器配置:
from openjiuwen.harness.lsp import InitializeOptions, CustomServerConfig rail = LspRail( options=InitializeOptions( cwd="/path/to/repo", custom_servers={ "pyright": CustomServerConfig( command="/usr/local/bin/pyright-langserver", args=["--stdio"], env={"PYRIGHT_PYTHON_PATH": "/usr/bin/python3"}, ) }, ), verbose=True, # write diagnostic snapshots to logs/logs/lsp/ )LspRail构造参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
options | InitializeOptions \| None | None | LSP 初始化选项(cwd、自定义服务器),默认继承 Agent 的 workspace |
verbose | bool | False | 为True时,每次before_model_call的诊断快照写入logs/logs/lsp/lsp_YYYYMMDD_HHMMSS.log |
CustomServerConfig字段:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
command | str \| None | None | 服务器可执行文件路径 |
args | list[str] \| None | None | 命令行参数 |
env | dict[str, str] \| None | None | 额外环境变量 |
extensions | list[str] \| None | None | 要处理的文件扩展名(如[".py"]) |
language_id | str \| None | None | LSP 语言标识符 |
initialization_options | dict \| None | None | 在initialize请求中传给服务器的选项 |
disabled | bool | False | 设为True可完全禁用该服务器 |
InitializeOptions字段:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
cwd | str \| None | None | 工作目录,默认取 Agent 的 workspace |
custom_servers | dict[str, CustomServerConfig] \| None | None | 按服务器 ID 键控的逐服务器覆盖配置 |
以上字段定义与 openjiuwen/harness/lsp/types.py 中的 dataclass 完全一致。自定义配置的合并逻辑见 openjiuwen/harness/lsp/servers/registry.py:disabled=True的服务器会从配置列表中移除;若服务器 ID 已存在(内置服务器),则仅覆盖command/args/initialization_options非空字段;若不存在则按extensions+language_id新建配置。
内置语言服务器支持
| 服务器 ID | 语言 | 文件扩展名 | 可执行文件 |
|---|---|---|---|
pyright | Python | .py,.pyi | pyright-langserver |
typescript | TypeScript / JavaScript | .ts,.tsx,.js,.jsx | typescript-language-server |
rust | Rust | .rs | rust-analyzer |
go | Go | .go | gopls |
java | Java | .java | jdtls |
内置服务器定义注册在BUILTIN_SERVERS全局注册表中(见 openjiuwen/harness/lsp/servers/registry.py 及各语言的 servers/servers/python.py、typescript.py、rust.py、go.py、java.py)。初始化时若找不到某个服务器二进制,该服务器会被静默跳过(注册为 command 为空的占位配置),其余语言服务器不受影响。
以 pyright 为例,其解析逻辑(_resolve_pyright_command)在 Windows 上通过npm list -g --depth=0 pyright获取全局 npm 前缀,再构造node <prefix>/node_modules/pyright/langserver.index.js --stdio;失败时回退到解析pyright-langserver.cmd包装脚本。同时,_spawn_python会自动探测VIRTUAL_ENV、.venv、venv中的 Python 解释器,并把pythonPath写入initialization_options,让 pyright 正确解析项目虚拟环境。
项目根目录发现(支持 monorepo)
LSPServerManager采用"扩展名 → 服务器 → 项目根"的多级匹配(见 openjiuwen/harness/lsp/core/manager.py):
- 按文件扩展名匹配候选服务器 ID 列表;
- 调用各服务器的
find_root(由nearest_root生成)向上遍历目录,寻找最近的包含pyproject.toml、setup.py、requirements.txt等标记文件的目录,遇到.git目录则停止; - 以
(server_id, root)构成缓存键(ServerInstanceKey),从而让同一语言在不同子项目中各自拥有独立服务器实例。
服务器启动过程包含完整的 LSP 握手等待与崩溃恢复:实例以startup_timeout(默认 45 秒)等待启动完成;僵尸实例(running 但失联)和 ERROR 实例会被清理并重启;启动超时会取消任务并清理缓存条目,防止残留"僵尸缓存"。所有服务器实例的启动、通知、请求统一由LSPServerInstance管理与LSPServerManager.send_request分发。
无 Agent 场景:底层initialize_lsp/call_lsp_toolAPI
除了通过DeepAgent+LspRail使用,LSP 子系统也提供底层 API,可直接在非 Agent 流程中调用:
initialize_lsp(options):初始化 LSP 子系统(幂等、懒加载混合模式)。初始化只构建配置映射,服务器在首次 LSP 请求时才启动。实现位于 openjiuwen/harness/lsp/init.py,底层走LSPServerManager.initialize。shutdown_lsp():关闭整个 LSP 子系统,停止所有服务器进程(含 5 秒的 spawn 任务取消等待)。get_pending_lsp_diagnostics(max_per_file=10, max_total=30):从全局注册表读取并清空待交付诊断。get_lsp_status():返回LspStatus(initialized, servers),列出各服务器 ID、运行状态、根目录、崩溃次数与最近错误。call_lsp_tool(input_data, workspace=None, operation=None):底层执行入口(openjiuwen/harness/tools/lsp_tool/_tool.py),LspTool.invoke即封装它。它负责:Pydantic 校验输入 → 解析/校验文件路径(含 workspace sandbox 边界检查)→ 若管理器未初始化则自动补初始化 → 获取/启动服务器 → 自动didOpen文件 → 组装 LSP 参数(1-indexed → 0-indexed)→ 对导航类操作做 gitignore 过滤 → 格式化结果。
从源码看,call_lsp_tool还会自动过滤位于 gitignored 目录(如node_modules、__pycache__)中的导航结果(filter_git_ignored_locations),并跳过超过 10MB(MAX_LSP_FILE_SIZE_BYTES)的大文件,避免把超大文件发送给语言服务器。这些限制同时在 openjiuwen/harness/prompts/tools/lsp_tool.py 的双语工具描述中显式告知 LLM。
测试与验证
LSP 集成相关的单元测试可直接作为行为契约参考:
- tests/unit_tests/harness/rails/test_lsp_rail.py:覆盖
LspRail的初始化、LspTool注册与uninit清理,使用_FakeDeepAgent与 mock 隔离外部依赖; - tests/unit_tests/harness/tools/test_lsp_tool.py 与 test_lsp_diagnostics.py:覆盖工具输入校验、参数转换、诊断注册表去重/封顶/排序语义。
通过这些测试与上述源码路径,可以完整追溯LspRail从工具注册、懒加载启动、didOpen/didChange触发、publishDiagnostics缓冲,到before_model_call注入UserMessage的整条链路。这也正是 openJiuwen agent-core 把"代码导航"与"诊断反馈"两种能力以声明式 Rail 形式注入 Agent 生命周期的核心设计。
- 人工智能
- AI Agent
- Agent 框架
- 大模型
- 工具调用
- RAG
- 提示工程
- 强化学习
【免费下载链接】agent-core
openJiuwen agent-core可提供AI Agent开发、运行、调优与演进相关的全套SDK能力
相关推荐
MariaDB 新 Binlog 实现详解:innodb 存储引擎下的文件格式、GTID 复制与平滑迁移
MariaDB 新 Binlog 实现详解:innodb 存储引擎下的文件格式、GTID 复制与平滑迁移 本文围绕 MariaDB 仓库中的 Docs/repl
人工智能AI AgentAgent 框架大模型工具调用RAG提示工程强化学习K8sGPT终极指南:如何用AI为Kubernetes集群赋予超级诊断能力
K8sGPT终极指南:如何用AI为Kubernetes集群赋予超级诊断能力 K8sGPT是一款革命性的AI工具,专门为Kubernetes集群管理而生。这款开源
云原生运维AI 应用MCP 服务OpenManus 工具系统深度指南:从 BaseTool 到 ToolCollection,为 Agent 赋予行动能力
OpenManus 工具系统深度指南:从 BaseTool 到 ToolCollection,为 Agent 赋予行动能力 本篇技术指南以 docs/OpenM
人工智能AI 应用AI Agent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考