去年,我开始对LangChain4J进行了一点研究。它是一个发展迅速的项目,我想要了解它的最新更新。我还想看看如何在LangChain4J中集成Model Context Protocol服务器。
版本 1 Beta 版我在2024年11月写了最后一篇博客文章,并使用了当时最新版本v0.35。LangChain4J从上个月底开始向1.0版本迈进。
日期 | 发布版本 |
---|---|
2024年9月25日 | 0.35.0 |
2024年12月22日 | 1.0.0-alpha1 |
2025年2月10日 | 1.0.0-beta1 |
2025年3月13日 | 1.0.0-beta2 |
2025年4月12日 | 1.0.0-beta3 |
LangChain4J 遵循 SemVer 版本规范。维护者借此机会引入了一些破坏性改动。在我的情况中,我不得不更新我的代码以适应破坏性 API 变更。
v0.35 | v1.0.0-beta3 |
---|
val s = Sinks.many()
.unicast()
.onBackpressureBuffer()
chatBot.talk(m.sessionId, m.text)
.onNext(s::tryEmitNext)
.onError(s::tryEmitError)
.onComplete {
s.tryEmitComplete()
}.start()
return ServerResponse.ok().bodyAndAwait(
s.asFlux().asFlow()
)
Note: The original source code is left in its original form as the corresponding Chinese technical terms or implementations are not widely standardized or recognized in the Chinese programming community. Thus, keeping the original code ensures clarity and technical accuracy for a Chinese-speaking programmer.
val s = Sinks.many()
.unicast()
.onBackpressureBuffer()
chatBot.talk(m.sessionId, m.text)
.onPartialResponse(s::tryEmitNext)
.onError(s::tryEmitError)
.onCompleteResponse {
s.tryEmitComplete()
}.start()
return ServerResponse.ok().bodyAndAwait(
s.asFlux().asFlow()
)
Project Reactor整合
注:代码未翻译,直接引用原英文代码。
LangChain4J 有一个 Project Reactor 集成;在我之前的想法中忽略了这一点。使用 Kotlin 协程,它使代码简化了很多。
因此,我在使用 AiServices
,所以我之前定义了一个接口,供 LangChain4J 在运行时实现:
interface 聊天机器人 {
fun 对话(@会话ID sessionId: String, @用户消息 message: String): Token流
}
全屏显示,退出全屏
我们需要添加以下依赖项:
<依赖>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
<version>1.0.0-beta3</version>
</依赖>
点击全屏 点退出
我们现在可以将返回类型从 Flux<String>
改为 TokenStream
。下面是更新后的签名:
interface ChatBot {
def 对话方法(@MemoryId 会话ID: String, @UserMessage 消息: String): Flux<String>
}
全屏,退出全屏
它使得上面不再需要创建 sink
。我们可以把代码变得更简洁,如下所示:
val flux = chatBot.talk(m.sessionId, m.text)
// 服务器响应成功,获取并等待flux转换为流
ServerResponse.ok().bodyAndAwait(flux.asFlow())
切换到全屏 退出全屏
记得两天调试时间可以帮你省下两小时查文档的时间,我没有去看文档。
把模型协议上下文集成到服务器功能中到目前为止,我们的改动很小。接下来在这部分里,我打算在我的LangChain4J应用中加入一个MCP。
检索增强生成
训练一个大型语言模型需要很多很多的资源,这直接意味着时间和金钱的投入。因此,公司会限制新模型版本的训练。随着时间的推移,信息的积累和变化会导致模型的相关性下降,而大型语言模型的数据库是不可变的。此外,大型语言模型本质上是基于公开数据进行训练的,而大多数公司也希望查询他们的私有数据。
检索增强生成(RAG)是传统上用来应对这些限制的方法。这个过程分为两个步骤。第一步,工具解析数据,根据大语言模型将其向量化并存储在向量数据库中。第二步,在查询大语言模型时,工具会从数据库中获取额外的信息。
模型上下文协议
应对LLM的静态特性的最新方法是一种MCP。
MCP 是一个开放协议,它规范了应用程序如何为大模型提供上下文。可以把 MCP 想象成 AI 应用的 USB-C 接口。就像 USB-C 提供了一种标准方式来连接你的设备与各种外围设备和配件一样,MCP 为连接 AI 模型与不同的数据源和工具提供了一种标准方式。
MCP 相比 RAG 有两个优势:
- RAG 处理的数据是为特定模型准备的。如果想使用新的模型,就需要重新解析数据。MCP 标准化了客户端和服务器之间的交互,使其技术无关。
- RAG 允许读取数据。MCP 允许任何 API 调用,既能动态访问数据,也能执行操作!
MCP 规定了客户端与服务器通信的两种传输选项:
- 通过标准输入输出(stdin/stdout)进行通信,基于HTTP的SSE(服务器发送事件)
- 客户端启动一个子进程并与其通信,基于HTTP的SSE(服务器发送事件)
设计解决方案架构
在上面的理论之后,我们现在可以动手实践了。首先,你需要选择一个MCP服务器。这里是一个不错的起点。不过,因为我看到LangChain4J的文档提到了它,所以我也选择了GitHub官方的MCP服务器。
GitHub MCP 服务器提供 stdio 传输。这意味着我们需要从应用中获取二进制文件并启动它。相比 HTTP 传输,它更快,但考虑到整体时间包括 HTTP 调用模型和它那边的计算时间,这其实并不重要。从架构角度来看,我更喜欢有一个专门的组件来运行。
经过一番研究后,我发现了一个mcp-proxy项目。它可以让你在stdio和HTTP之间进行切换,既可以将stdio转换为HTTP,也可以将HTTP转换回stdio。也可以通过Docker镜像来使用该项目。我们可以在下面的Dockerfile
中结合服务器和代理。
FROM ghcr.io/sparfenyuk/mcp-proxy:latest
ENV VERSION=0.2.0
ENV ARCHIVE_NAME=github-mcp-server_Linux_x86_64.tar.gz
RUN wget https://github.com/github/github-mcp-server/releases/download/v$VERSION/$ARCHIVE_NAME -O /tmp/$ARCHIVE_NAME \ # 下载指定版本的归档文件到临时目录
&& tar -xzvf /tmp/$ARCHIVE_NAME -C /opt \ # 解压归档文件到指定目录
&& rm /tmp/$ARCHIVE_NAME # 删除临时文件
RUN chmod +x /opt/github-mcp-server # 赋予执行权限
点击全屏按钮进入全屏,再点击一次退出全屏
- 下载档案文件
- 解压文件
- 删除档案文件
- 将二进制文件设置为可执行
需要注意的是,我们不能将 CMD
仅作为二进制文件使用,因为它仅支持通过参数设置端口和主机。因此,我们必须推迟执行该命令,在我的情况中,是在 docker-compose.yaml
中。
services:
mcp-server:
build:
context: github-mcp-server
env_file:
- .env #1
command:
- --pass-environment #2
- --sse-port=8080 #3
- --sse-host=0.0.0.0 #4
- -- #5 空选项或占位符
- /opt/github-mcp-server #6
- --toolsets
- all
- stdio
全屏 全屏退出
- 我们需要一个有效的
GITHUB_PERSONAL_ACCESS_TOKEN
环境变量,用于在 GitHub 上进行身份验证 - 将所有环境变量传递给子进程执行
- 设置监听端口
- 绑定到任何 IP
- 代理在破折号后连接到 stdio MCP 服务器
- 启用所有选项后运行服务器
图片会在8080端口展示 /sse
端点。
写代码解决
编码部分是最简单的部分。直接访问LangChain4J 的 MCP 文档并按照指示进行。在项目里,内容如下:
bean {
val 传输 = HttpMcpTransport.Builder()
.sseUrl(ref<ApplicationProperties>().mcp.url) //1
.logRequests(true) //2
.logResponses(true) //2
.build()
val mcpClient = DefaultMcpClient.Builder()
.transport(传输)
.build()
mcpClient.listTools().forEach { println(it) } //3
McpToolProvider.builder()
.mcpClients(listOf(mcpClient))
.build()
}
bean {
coRouter {
val chatBot = AiServices
.builder(ChatBot::class.java)
.streamingChatLanguageModel(ref<StreamingChatLanguageModel>())
.chatMemoryProvider { MessageWindowChatMemory.withMaxMessages(40) }
.contentRetriever(EmbeddingStoreContentRetriever.from(ref<EmbeddingStore<TextSegment>>()))
.toolProvider(ref<McpToolProvider>()) //4
.build()
POST("/")(PromptHandler(chatBot)::handle)
}
}
请点击进入全屏模式,关闭全屏模式
- 我添加了一个
ConfigurationProperty
类来指定 SSE URL 的参数 - MCP 协议提供了一种将日志发送回客户端的方法
- 虽然不必要,但有助于确保客户端连接到服务器并列出提供的工具
- 将上面创建的 MCP 工具提供者集成到
AiServices
中
在这个阶段,模型需要将任何匹配已注册工具的请求转发给MCP服务器。
curl -N -H 'Content-Type: application/json' localhost:8080 -d '{ "sessionId": "1", "text": "我最流行的三个GitHub仓库是哪几个?" }'
点击全屏按钮来切换全屏模式
我试了好几次,得到了类似这样的回复:
遗憾的是,提供的这段文本并没有关于你最流行的三个GitHub仓库的具体信息。这段文本似乎是博客文章或个人网站,提到了一些你的项目和在GitHub上的经历,但并没有提供关于仓库受欢迎程度的具体数据。
如果你想了解更多关于你的GitHub仓库受欢迎程度的具体数据,我建议你可以查看GitHub自己的分析工具,例如GitHub Insights或Repository Insights API。这些工具有助于查看每个仓库的关注者人数、星标数量和分叉数量,以及其他相关的活跃度和参与度指标。
全屏模式 退出全屏
模型忽略了工具,虽然文档说不是这样。
解决问题
我反复阅读了LangChain4J的文档,但还是没有成功。我还尝试了OpenAI和其他几个AI工具,但都没有成功。大多数答案都说可以直接使用,但有人提到需要直接调用工具,这显然不符合我的初衷。还有人提到Ollama不支持工具。我几乎花了整整一天的时间,想知道我哪里出了错。
解耦架构引入了更多的组件,这使得整个系统更加复杂。我怀疑整个调用链可能出了问题。我移除了MCP代理程序,将github-mcp-server
直接集成到应用镜像中,并将代码从使用HTTP改为使用stdio。但这并没有解决问题,问题依旧存在。
正要放弃的时候,我决定回归原点。我复制粘贴了文档中的示例代码:它竟然直接就运行起来了!那一刻我真是得意极了。
这里用的是OpenAI,而我用的是Ollama。我试了MCP搭配OpenAI、Mistral AI以及Ollama。只有OpenAI的模型能与MCP协同工作。我发了同样的请求:
curl -N -H 'Content-Type: application/json' localhost:8080 -d '{ "sessionId": "1", "text": "我最流行的三个GitHub仓库是哪三个?" }'
全屏 退出全屏
现在,OpenAI 正确地将请求映射到了正确的工具,并返回了我期望得到的答案:
以下是关于您最流行的三个 GitHub 仓库的发现结果:
-
- 描述:通过 OpenTelemetry 进行端到端跟踪的演示。
- Star 数:68
- Fork 数:25
- 未解决的问题:10
-
- 描述:基于 Kotlin 的 DSL 用于 Vaadin。
- Star 数:44
- Fork 数:12
- 未解决的问题:3
- jvm-controller
- 描述:使用 Java 编写 Kubernetes Controller 的示例。
- Star 数:33
- Fork 数:10
- 未解决的问题:0
这些仓库展示了您在可观测性(即监控和跟踪系统的性能)、Kotlin 开发和 Kubernetes 方面的兴趣所在和贡献情况。
全屏 退出全屏
因为我们向MCP服务器传递了一个认证令牌,MCP服务器将其传递给GitHub API,因此GitHub API知道是谁发起了这次调用。因此,它能够理解上述查询中的我的仓库部分。我承认对于大多数常规的、面向多个用户的web应用程序来说,使用单一认证令牌的做法不常见,但这种做法对桌面应用程序来说再合适不过了。
其他常见的问题,比如在 GitHub 上找到最热门的仓库,与网页应用相关,因为这些问题没有明确指出用户。
最后来总结一下本文主要讨论在LangChain4J应用中集成一个MCP服务器。配置相对简单,根据相关文档,但仍有一些注意事项。
首先,MCP 服务器如何融入你的架构完全取决于你。我不得不创造性地使用了出色的 mcp-proxy
来使它解耦。然后,看起来 LangChain4J 是一个不完善的抽象。它使一切变得可能,为了给你提供一个强大的抽象层,但隐藏在其下的实现并不都是一样的。我希望文档中能提到这一点,即使它处于测试阶段,尽管我知道它当前仍处于测试阶段。
总的来说,可以说这趟经历挺有趣的。我在现实世界中了解了MCP,也为我的项目想法提供了不少灵感和机会。
本文的完整源代码如下,可在 GitHub 网址上查看:
GitHub标志: ajavageek / langchain4j-musings更多内容:
(此处省略了内容)
原发布于A Java Geek,2025年4月27日
[MCP]:模型上下文协议(MCP)
[LLM]:大语言模型(LLM)
[RAG]:检索增强生成(RAG)
[SSE]:服务器发送事件(SSE)
共同學習,寫下你的評論
評論加載中...
作者其他優(yōu)質文章