☰
openJiuwen agent-core LSP 深度指南:用 LspRail 为 DeepAgent 赋予代码导航与诊断注入能力
2026/10/10 2:28:03 网站建设 项目流程
  • 人工智能
  • AI Agent
  • Agent 框架
  • 大模型
  • 工具调用
  • RAG
  • 提示工程
  • 强化学习

【免费下载链接】agent-core

openJiuwen agent-core可提供AI Agent开发、运行、调优与演进相关的全套SDK能力

项目地址:https://gitcode.com/openJiuwen/agent-core
点击查看免费下载

本文是一份面向 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操作说明
1goToDefinition跳转到函数的定义
2findReferences查找符号的所有使用位置
3documentSymbol列出文件内全部符号
4workspaceSymbol跨整个项目搜索符号
5goToImplementation查找抽象方法的具体实现
6prepareCallHierarchy准备调用层级条目
7incomingCalls查找调用某函数的所有调用方
8outgoingCalls查找某函数调用的所有下游函数
9before_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):

  1. 从ctx.inputs读取tool_name,只有当其命中内部集合_WRITE_TOOL_NAMES = {"edit_file", "write_file"}时才继续;
  2. 解析编辑文件的绝对路径——相对路径按workspace.root_path或当前工作目录解析(在调用线程上提前 resolve,避免协程内 cwd 变化导致路径失效);
  3. 按扩展名推断language_id(.py/.pyi映射为"python",其余取去掉点号的扩展名);
  4. 若文件从未打开过,先发textDocument/didOpen再发textDocument/didChange——因为部分服务器(如 pyright)要求先didOpen才接受didChange;
  5. 立即返回,重新分析为 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 方法
documentSymboltextDocument/documentSymbol
goToDefinitiontextDocument/definition
findReferencestextDocument/references
workspaceSymbolworkspace/symbol
goToImplementationtextDocument/implementation
prepareCallHierarchytextDocument/prepareCallHierarchy
incomingCallscallHierarchy/incomingCalls
outgoingCallscallHierarchy/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 将依次调用:

  1. lsp(operation="changeFile", file_path="src/models.py", content="...")— 把新内容发给 pyright;
  2. 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构造参数:

参数类型默认值说明
optionsInitializeOptions \| NoneNoneLSP 初始化选项(cwd、自定义服务器),默认继承 Agent 的 workspace
verboseboolFalse为True时,每次before_model_call的诊断快照写入logs/logs/lsp/lsp_YYYYMMDD_HHMMSS.log

CustomServerConfig字段:

字段类型默认值说明
commandstr \| NoneNone服务器可执行文件路径
argslist[str] \| NoneNone命令行参数
envdict[str, str] \| NoneNone额外环境变量
extensionslist[str] \| NoneNone要处理的文件扩展名(如[".py"])
language_idstr \| NoneNoneLSP 语言标识符
initialization_optionsdict \| NoneNone在initialize请求中传给服务器的选项
disabledboolFalse设为True可完全禁用该服务器

InitializeOptions字段:

字段类型默认值说明
cwdstr \| NoneNone工作目录,默认取 Agent 的 workspace
custom_serversdict[str, CustomServerConfig] \| NoneNone按服务器 ID 键控的逐服务器覆盖配置

以上字段定义与 openjiuwen/harness/lsp/types.py 中的 dataclass 完全一致。自定义配置的合并逻辑见 openjiuwen/harness/lsp/servers/registry.py:disabled=True的服务器会从配置列表中移除;若服务器 ID 已存在(内置服务器),则仅覆盖command/args/initialization_options非空字段;若不存在则按extensions+language_id新建配置。

内置语言服务器支持

服务器 ID语言文件扩展名可执行文件
pyrightPython.py,.pyipyright-langserver
typescriptTypeScript / JavaScript.ts,.tsx,.js,.jsxtypescript-language-server
rustRust.rsrust-analyzer
goGo.gogopls
javaJava.javajdtls

内置服务器定义注册在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):

  1. 按文件扩展名匹配候选服务器 ID 列表;
  2. 调用各服务器的find_root(由nearest_root生成)向上遍历目录,寻找最近的包含pyproject.toml、setup.py、requirements.txt等标记文件的目录,遇到.git目录则停止;
  3. 以(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能力

项目地址:https://gitcode.com/openJiuwen/agent-core
点击查看免费下载

相关推荐

上一篇:大模型基础教材 PDF 获取指南:1 条命令一次拿全 8 份
下一篇:终极窗口置顶神器:AlwaysOnTop 完整使用指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询