ipfans's Bloghttps://www.4async.com/Recent content on ipfans's BlogHugo -- gohugo.ioenSat, 31 May 2025 20:30:00 +0000程序员的提示工程手册https://www.4async.com/2025/05/prompt-engineering-playbook-for-programmers/Sat, 31 May 2025 20:30:00 +0000https://www.4async.com/2025/05/prompt-engineering-playbook-for-programmers/<img src="https://www.4async.com/2025/05/prompt-engineering-playbook-for-programmers/cover.webp" alt="Featured image of post 程序员的提示工程手册" /><blockquote> <p>原文来自于 Addy Osmani,他是 Google Chrome 的工程主管,在 Google 工作多年。</p></blockquote> <p>开发人员越来越依赖 AI 编程助手来加速我们的日常工作流程。这些工具可以自动完成函数、建议修复错误,甚至生成整个模块或 MVP。然而,正如我们许多人所了解的那样,AI 输出的质量很大程度上取决于你提供的提示的质量。换句话说,提示工程已经成为一项必备技能。一个措辞不当的请求可能会产生不相关或笼统的答案,而一个精心设计的提示可以产生周到、准确甚至富有创造性的代码解决方案。本文将实际探讨如何系统地构建适用于常见开发任务的提示。</p> <p>AI 结对程序员很强大但并非神奇——除了你告诉他们的或包含在上下文中的信息外,对你特定的项目或意图没有先验知识。你提供的信息越多,输出就越好。我们将提炼出开发者反响热烈的提示模式、可重复的框架和难忘的示例。你将看到好与坏提示的并排比较以及实际的 AI 响应,并附有评论以了解为什么一个成功而另一个失败。以下是入门小贴士:</p> <table> <thead> <tr> <th>技巧</th> <th>提示模板</th> <th>目的</th> </tr> </thead> <tbody> <tr> <td>1. 角色提示</td> <td>“你是一名资深的{语言名}开发者。请审查这个函数以实现{目标}。”</td> <td>模拟专家级的代码审查、调试或重构</td> </tr> <tr> <td>2. 明确上下文设定</td> <td>“问题如下:{问题说明}。下面是代码。它本应做到{预期行为},但实际上做了 {实际行为}。为什么?”</td> <td>明确框定问题,避免泛泛而谈的回答</td> </tr> <tr> <td>3. 输入/输出示例</td> <td>“这个函数在输入{输入内容}时应返回{预期输出}。你能编写或修复这段代码吗?”</td> <td>通过示例引导 AI 理解意图</td> </tr> <tr> <td>4. 迭代分步</td> <td>“首先生成这个组件的骨架。接下来我们添加状态。然后处理 API 调用。”</td> <td>将大任务拆解为步骤,避免模糊或过载的提示</td> </tr> <tr> <td>5. 模拟调试</td> <td>“逐行走查这个函数。变量值是什么?哪里可能出错?”</td> <td>引导 AI 模拟运行时行为,发现隐藏的 bug</td> </tr> <tr> <td>6. 功能蓝图</td> <td>“我正在构建{功能特性}。需求如下:{具体说明}。技术栈为:{技术栈说明}。请搭建初始组件并解释你的选择。”</td> <td>启动 AI 主导的功能规划与脚手架搭建</td> </tr> <tr> <td>7. 重构指导</td> <td>“请重构这段代码以提升{目标},比如(如:可读性、性能、符合语言习惯的风格)。请使用注释解释变更。”</td> <td>让 AI 的重构符合你的目标,而不是随意更动</td> </tr> <tr> <td>8. 请求替代方案</td> <td>“你能用函数式风格重写这段代码吗?递归版本会是什么样?”</td> <td>探索多种实现方式,扩展工具箱</td> </tr> <tr> <td>9. 橡皮鸭调试</td> <td>“我觉得这个函数的作用是:{你的解释}。我遗漏了什么吗?有没有 bug?”</td> <td>让 AI 质疑你的理解并发现潜在问题</td> </tr> <tr> <td>10. 约束锚定</td> <td>“请避免(例如:递归),并遵守(例如:ES6 语法、无外部库)。优化方向为(例如:内存)。这是函数代码:”</td> <td>防止 AI 过度发挥或引入不兼容的模式</td> </tr> </tbody> </table> <h2 id="高效代码提示基础">高效代码提示基础 </h2><p>向 AI 编码工具提示与与一个非常有原则、有时会知识渊博的合作者交流有点像。为了获得有用的结果,您需要清楚地设置场景,并指导 AI 您想要什么以及如何实现。</p>AI 产品的破局之道:以人为本https://www.4async.com/2024/12/human-centered-product-design-with-human-in-the-loop/Sun, 29 Dec 2024 22:30:00 +0000https://www.4async.com/2024/12/human-centered-product-design-with-human-in-the-loop/<img src="https://www.4async.com/2024/12/human-centered-product-design-with-human-in-the-loop/cover.webp" alt="Featured image of post AI 产品的破局之道:以人为本" /><p>伴随着 OpenAI、Anthropic 等等公司的努力,大型语言模型(LLM)的性能不断提升,AI 产品的应用场景也越来越广泛。从口袋里的智能助手到驰骋未来的自动驾驶,从辅助医疗诊断的慧眼到洞察市场风云的智脑,AI 正以前所未有的速度改变着世界。但是在这个过程中,我们会注意到一些问题,我们会陷入“技术至上”的误区,过分强调技术的先进性,而忽略了用户的需求和体验。这就导致了很多 AI 产品虽然技术先进,但却无法真正满足用户的需求,最终无法实现商业价值。究其原因,许多 AI 系统在与人类交互时,常常陷入**“鸡同鸭讲”的窘境**——对上下文的理解捉襟见肘,难以领会人类语言中的微妙含义;更令人担忧的是“幻觉”问题,AI 煞有介事地编造着“一本正经的胡说八道”,让用户无所适从;而在用户体验方面,许多 AI 产品更是差强人意,机械式的交互和冰冷的界面让人难以产生亲近感。有些时候我们有些失望:<a class="link" href="https://zh.wikipedia.org/zh-hans/%E5%B8%95%E7%B4%AF%E6%89%98%E6%B3%95%E5%88%99" target="_blank" rel="noopener" >帕累托法则</a>(或者叫做 28 原则),一直在发挥着神秘的力量。</p> <p>抛开模型开发公司本身而言,面向客户的 AI 产品的设计和开发是一个复杂的过程,需要综合考虑技术、商业和用户体验等多个方面。在这个过程中,如何保证产品的质量和用户体验成为了关键问题。传统的产品设计方法往往是由设计师和工程师等专业人员完成,而用户往往只是产品的使用者。然而,随着 LLM 的发展,新的交互形式不停的出现,比如之前的聊天产品和语音助手交互产品普遍反馈交互感太差并不实用。新的实时多模态模型的出现,让这些可以更好的理解用户的需求,更好的满足用户的需求。</p> <p>但,无论如何,要充分释放 AI 的潜力,必须将用户需求和体验置于核心地位。这不仅仅是一句口号,更是一种深刻的变革,要求我们将用户的需求、期望和体验置于 AI 产品设计的最核心。这意味着,我们需要像人类学家一样去观察和理解用户的真实诉求,像心理学家一样去揣摩用户的情感和认知模式,并将这些洞察深深地烙印在产品的基因之中。只有这样,才能打造出真正被用户拥抱、信任并从中受益的 AI 产品。</p> <p>本文就是我在最近两年开发 AI 产品的一些体悟,希望能够给 AI 产品的设计和开发带来一些启发。虽然我是一个工程师视角,但是斗胆今天就来聊一聊 AI 产品的设计和体验问题。</p> <h2 id="理解当前-ai-的阶段能力与局限">理解当前 AI 的阶段:能力与局限 </h2><p>毋庸置疑,现在 AI 在“力大砖飞”的背景之下,已经在许多领域展现出了强大的能力。从自然语言处理到计算机视觉,从智能推荐到智能对话,AI 的应用场景越来越广泛,技术也越来越成熟。在过去的两年中,我们见证了每隔几个月到半年,LLM 就进行了一种大范围的能力提升变革。然而,AI 的能力和局限性也给产品设计带来了许多挑战。这些局限性并非是无法克服的,有些也仅仅局限于当前的技术水平,但是我们必须清楚地认识到这些局限性,才能更好地设计和开发 AI 产品。</p> <p>这些问题你可能在很多地方看到过,让我再赘述一遍:</p> <ol> <li><strong>上下文问题</strong>:大模型在处理长文本或复杂对话时,受限于上下文“记忆”能力,可能导致理解偏差和信息遗漏。例如,在长篇文档总结或多轮对话中难以保持信息连贯。扩展上下文窗口当然是一种解决方案,但即便是大上下文窗口的 Gemini,也会出现注意力不集中的问题,最终影响到模型的性能和结果。</li> <li><strong>幻觉问题</strong>:AI 可能在缺乏事实依据的情况下生成看似合理但虚假的信息,严重影响用户信任,尤其在需要高准确性的场景中。在绝大多数的 B 端场景和部分 C 端场景中,我们都需要确保回答的准确性。之前也有<a class="link" href="https://news.qq.com/rain/a/20240223A012Z100" target="_blank" rel="noopener" >加拿大航空的案例</a>,导致公司的信誉受损。</li> <li><strong>缺乏常识与情感理解</strong>: AI 在处理需要常识判断和情感理解的场景中表现不足,导致交互生硬,甚至产生误解。</li> </ol> <p>这些问题并非无法解决,但是我们必须清楚地认识到这些问题,才能更好地设计和开发 AI 产品。在 AI 产品设计中,我们需要充分考虑这些问题,从而更好地满足用户的需求和体验。而在这个过程中,我认为针对人机回环(Human in the loop, HITL)的设计思想是非常重要的。通过完善这个设计思想,我们可以更好地解决上述问题,提高 AI 产品的质量和用户体验。</p> <h2 id="以人为本产品的设计灵魂">以人为本:产品的设计灵魂 </h2><p>以人为本,是产品设计的灵魂所在,对于 AI 产品而言更是如此。对这种方式而言,一般会以下面的方式为循环来进行:</p>使用 Go 开发 AI Agent的选择:Genkit for Gohttps://www.4async.com/2024/11/building-ai-agent-with-genkit-for-go/Thu, 14 Nov 2024 11:28:00 +0000https://www.4async.com/2024/11/building-ai-agent-with-genkit-for-go/<img src="https://www.4async.com/2024/11/building-ai-agent-with-genkit-for-go/cover.png" alt="Featured image of post 使用 Go 开发 AI Agent的选择:Genkit for Go" /><h2 id="什么是-genkit">什么是 Genkit </h2><p><a class="link" href="https://github.com/firebase/genkit" target="_blank" rel="noopener" >Genkit</a> 是一个 Google Firebase 团队开发的 AI Agent 开发框架,用于构建现代、高效的 AI 应用。它目前包含一个 <a class="link" href="https://firebase.google.com/docs/genkit?hl=zh-cn" target="_blank" rel="noopener" >Node.js 的实现</a> 和一个 <a class="link" href="https://firebase.google.com/docs/genkit-go/get-started-go" target="_blank" rel="noopener" >Go 语言的实现</a>。之所以注意到这个框架是因为 Go 团队在他们的<a class="link" href="https://go.dev/blog/15years" target="_blank" rel="noopener" >十五周年博客</a>中提到了它。Go 团队在博客中提到,他们正在努力使 Go 成为构建生产 AI 系统的优秀语言,并且他们正在为流行的 Go 语言框架提供支持,包括 <a class="link" href="https://github.com/tmc/langchaingo" target="_blank" rel="noopener" >LangChainGo</a> 和 <a class="link" href="https://developers.googleblog.com/en/introducing-genkit-for-go-build-scalable-ai-powered-apps-in-go/" target="_blank" rel="noopener" >Genkit</a>。如果你了解过 AI 开发,那么 <a class="link" href="https://www.langchain.com/" target="_blank" rel="noopener" >LangChain</a> 一定并不陌生。LangChainGo 就是 LangChain 的 Go 语言实现。但是对应的,我们还需要一个单独的 AI Agent 开发框架,帮助我们组织 AI Agent 的开发。LangChain 的生态位中,对应的是 <a class="link" href="https://www.langchain.com/langgraph" target="_blank" rel="noopener" >LangGraph</a>,在 Go 语言的生态位中,你可以选择 Genkit 或者 <a class="link" href="https://github.com/tmc/langgraphgo" target="_blank" rel="noopener" >LangGraphGo</a>。</p> <p>但是,值得注意的是,目前 Genkit 仍旧是在 Alpha 的状态,所以,<strong>如果你希望将 Genkit 用于生产环境,在 2024 年这个时间点请注意未来可能的 API 变动。</strong> 目前 Genkit 官方支持的模型功能仍旧是以 Google 家的产品为主,如果你需要使用其他 AI 模型,你可能需要暂时先使用第三方 plugin(比如 OpenAI)。</p>RAG 技术在实际工程中的应用:OpenAI 的最佳实践分享https://www.4async.com/2024/06/openai-survey-of-techniques-for-maximizing-llm-performance/Mon, 03 Jun 2024 11:15:00 +0000https://www.4async.com/2024/06/openai-survey-of-techniques-for-maximizing-llm-performance/<img src="https://www.4async.com/2024/06/openai-survey-of-techniques-for-maximizing-llm-performance/cover.png" alt="Featured image of post RAG 技术在实际工程中的应用:OpenAI 的最佳实践分享" /><p>检索增强生成(Retrieval Augmented Generation,简称RAG)技术正逐渐成为提升大型语言模型(LLM)性能的关键。OpenAI 的 John Allard 和 Colin Jarvis 在<a class="link" href="https://www.youtube.com/watch?v=ahnGLM-RC1Y" target="_blank" rel="noopener" >一场分享</a>中,以实际案例为基础,深入浅出地讲解了如何将 RAG 技术应用于实际工程问题,并分享了他们的最佳实践经验。这些内容对于 RAG 技术的应用者来说,无疑是非常有价值的。事实上,OpenAI 开放了很多的他们的最佳实践内容,包括不限于 Prompt Engineering、Data Augmentation、Fine-tuning、Inference Optimization 等等。有空除了多看一下相关论文,OpenAI 的 Github 和 Youtube 频道都值得看一下。比如说这次分享就是来源于 OpenAI 的 Youtube 频道。</p> <h2 id="优化-llm-性能的挑战">优化 LLM 性能的挑战 </h2><p>优化 LLM 性能并非易事,哪怕丁点的区别都可能导致结果上出现很大的差异。目前优化性能主要存在以下挑战:</p> <ul> <li><strong>难以确定问题根源</strong>: LLM 的输出结果受多种因素影响,如提示工程、训练数据、模型架构等。当模型性能不佳时,难以从这些复杂的交互中分离出导致问题的具体原因。</li> <li><strong>难以衡量性能</strong>: 对于许多 LLM 应用,例如文本摘要、问答系统等,难以找到一个客观的、可量化的指标来准确评估模型的性能。</li> <li><strong>难以选择优化方法</strong>: 即使确定了问题,也难以选择最合适的优化方法。开发者需要根据具体问题选择合适的技术,例如提示工程、微调、RAG 等。</li> </ul> <h2 id="openai-的优化框架">OpenAI 的优化框架 </h2><p>为了解决上述挑战,OpenAI 提出了一个二维优化框架,将优化方向分为__内容优化__和__LLM优化__两个部分。</p> <h3 id="内容优化">内容优化 </h3><p>内容优化指的是优化模型可访问的知识内容。如果模型缺乏必要的知识或信息质量较差,那么即使模型本身的能力很强,也难以给出令人满意的答案。</p> <p>为了补全这部分的知识内容,可以采用以下方法进行:</p> <ul> <li><strong>RAG</strong>: 将外部知识库与 LLM 集成,允许模型访问更广泛、更专业的知识。</li> <li><strong>数据增强</strong>: 通过对现有数据进行扩展或转换,增加训练数据的数量和多样性。</li> <li><strong>数据清洗</strong>: 识别和纠正训练数据中的错误或噪声,提高数据质量。</li> </ul> <h3 id="llm-优化">LLM 优化 </h3><p>通过优化模型本身提升效果。如果模型本身存在缺陷,例如容易产生幻觉或难以理解复杂指令,那么即使有充足的知识,也难以给出合理的答案。</p> <p>在这个优化方案中,可以采用下面的方式进行:</p> <ul> <li><strong>提示工程</strong>: 通过设计更清晰、更明确的提示,引导模型生成符合预期的输出。</li> <li><strong>微调</strong>: 使用特定任务的数据集对预训练模型进行进一步训练,使模型更适应特定任务。</li> <li><strong>模型架构优化</strong>: 调整模型的架构或参数,提高模型的学习能力和泛化能力。</li> </ul> <h3 id="openai-的建议">OpenAI 的建议 </h3><p>在这个框架下,OpenAI 建议开发者采用以下流程进行优化:</p>从零学习 Hypothetical Document Embeddings (HyDE) - 2https://www.4async.com/2024/04/learn-hyde-from-scratch-2/Wed, 10 Apr 2024 09:15:00 +0000https://www.4async.com/2024/04/learn-hyde-from-scratch-2/<img src="https://www.4async.com/2024/04/learn-hyde-from-scratch-2/cover.png" alt="Featured image of post 从零学习 Hypothetical Document Embeddings (HyDE) - 2" /><p>在开始看这篇文章之前,推荐你阅读前一篇<a class="link" href="https://www.4async.com/2024/04/learn-hyde-from-scratch-1/" title="概念介绍文章" target="_blank" rel="noopener" >概念介绍文章</a>。这篇文章之内不会再额外介绍概念上的内容,仅仅从实践角度演示如何是用 HyDE 和 RAG 结合。正如同前文提到的那样,我们会是用 LangChain 构建这个演示 Demo。当然,如果你熟悉 LlamaIndex 或者其他框架,其实主要流程大致差不多,只不过是用的 API 和库有一些区别。这些就不再赘述。</p> <h2 id="问题背景">问题背景 </h2><p>让我们假设你在为麦当劳制作一个问答系统。这个问答系统的目的是帮助用户解决一些常见问题,比如:麦当劳的营业时间,麦当劳的菜单,麦当劳的优惠活动等等。这些问题的答案通常是可以在麦当劳的官方网站上找到的,但是用户可能并不知道这些信息,或者他们可能会用自己的方式提出问题。这时候,我们就需要一个 RAG 模型来帮助我们回答这些问题。但是麦当劳实际上有非常多的商品,那么它的知识库可能会非常大,而且用户提出的问题可能并不在知识库中。这时候 HyDE 就可以帮助我们处理这些问题。</p> <h2 id="准备工作">准备工作 </h2><p>我们会是用一些基本的 Python 相关工具(<a class="link" href="https://pdm.fming.dev/latest/" title="pdm" target="_blank" rel="noopener" >pdm</a> 等等)、<a class="link" href="https://ollama.com/" title="Ollama" target="_blank" rel="noopener" >Ollama</a>和<a class="link" href="https://www.trychroma.com/" title="chroma" target="_blank" rel="noopener" >chroma</a>。关于 Ollama 的介绍,可以查看<a class="link" href="https://www.4async.com/2024/02/deploy-local-llm-using-ollama/" title="我的之前的文章" target="_blank" rel="noopener" >我的之前的文章</a>。出于成本考虑,这里使用了开源的本地模型实现,请确保你的 Ollama 是在运行的状态。当然,如果你是用 <a class="link" href="https://platform.openai.com/docs/models/gpt-4-turbo-and-gpt-4" title="GPT-4 Turbo" target="_blank" rel="noopener" >GPT-4 Turbo</a>(今天凌晨刚刚发布了新版本),效果要远比开源模型好很多。</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>mkdir hydemo <span style="color:#f92672">&amp;&amp;</span> cd hydemo </span></span><span style="display:flex;"><span>pdm init </span></span><span style="display:flex;"><span>pdm add langchain chromadb beautifulsoup4 </span></span><span style="display:flex;"><span>source .venv/bin/activate </span></span></code></pre></div><blockquote> <p>请注意,在本文编写时,使用的 LangChain 的版本为 0.1.14,如果你的版本较低或者过高,可能存在部分 API 出入,请根据具体情况调整引用的模块和对应的 API。</p></blockquote> <h2 id="功能实现">功能实现 </h2><h3 id="实现生成模拟文档">实现生成模拟文档 </h3><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> langchain.chains.hyde.base <span style="color:#f92672">import</span> HypotheticalDocumentEmbedder </span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> langchain.chains.llm <span style="color:#f92672">import</span> LLMChain </span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> langchain.embeddings.ollama <span style="color:#f92672">import</span> OllamaEmbeddings </span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> langchain.globals <span style="color:#f92672">import</span> set_debug </span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> langchain.llms.ollama <span style="color:#f92672">import</span> Ollama </span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> langchain.vectorstores.chroma <span style="color:#f92672">import</span> Chroma </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">main</span>(): </span></span><span style="display:flex;"><span> set_debug(<span style="color:#66d9ef">True</span>) <span style="color:#75715e"># 设置 langchain 的调试模式,后面我们会看到具体的效果</span> </span></span><span style="display:flex;"><span> llm <span style="color:#f92672">=</span> Ollama(model<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;qwen:7b&#34;</span>) <span style="color:#75715e"># 使用通义千问 7b 模型</span> </span></span><span style="display:flex;"><span> olemb <span style="color:#f92672">=</span> OllamaEmbeddings( </span></span><span style="display:flex;"><span> model<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;nomic-embed-text&#34;</span> </span></span><span style="display:flex;"><span> ) <span style="color:#75715e"># 嵌入模型我们是使用的 nomic-embed-text</span> </span></span><span style="display:flex;"><span> embeddings <span style="color:#f92672">=</span> HypotheticalDocumentEmbedder<span style="color:#f92672">.</span>from_llm( </span></span><span style="display:flex;"><span> llm, </span></span><span style="display:flex;"><span> base_embeddings<span style="color:#f92672">=</span>olemb, </span></span><span style="display:flex;"><span> prompt_key<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;web_search&#34;</span>, <span style="color:#75715e"># 加载预置的 Web Search Prompt</span> </span></span><span style="display:flex;"><span> ) </span></span><span style="display:flex;"><span> print(embeddings<span style="color:#f92672">.</span>llm_chain<span style="color:#f92672">.</span>prompt) </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;__main__&#34;</span>: </span></span><span style="display:flex;"><span> main() </span></span></code></pre></div><p>让我们先执行一下,看一下 Prompt 会是什么样子的:</p>从零学习 Hypothetical Document Embeddings (HyDE) - 1https://www.4async.com/2024/04/learn-hyde-from-scratch-1/Tue, 09 Apr 2024 14:15:00 +0000https://www.4async.com/2024/04/learn-hyde-from-scratch-1/<img src="https://www.4async.com/2024/04/learn-hyde-from-scratch-1/cover.png" alt="Featured image of post 从零学习 Hypothetical Document Embeddings (HyDE) - 1" /><p>最近看到了一篇新的论文,关于评估 RAG 中的各种技术的评估。这篇<a class="link" href="https://arxiv.org/pdf/2404.01037.pdf" target="_blank" rel="noopener" >《ARAGOG: Advanced RAG Output Grading》</a>提供了一个对比多种 RAG 技术的评估。其中提到 HyDE 和 LLM re-rank 能够很好的提高检索精度。不过似乎 HyDE 其实讨论并不是很多,那么咱就对这块内容添砖加瓦一下,让我们从零开始进行一段 HyDE 的冒险。</p> <h2 id="什么是-rag">什么是 RAG? </h2><p>大语言模型(LLM)虽然在过去的一段时间中成为大家眼中的香馍馍,但它们也存在一些关键的局限性:</p> <ul> <li><strong>知识局限性</strong>: LLM 的知识范围主要局限于其训练数据,难以涵盖所有知识领域,在一些专业或冷门话题上表现不佳。尤其是现在 AI 应用场景需要使用来自于企业内部知识库或者外部知识库中的相关信息,而这些内部知识库往往是不在外部公开的。</li> <li><strong>可靠性问题</strong>: LLM 依赖于概率模型生成文本,结果难免存在事实性错误或缺乏可靠性,在某些关键应用中存在风险。比如之前的<a class="link" href="https://bc.ctvnews.ca/air-canada-s-chatbot-gave-a-b-c-man-the-wrong-information-now-the-airline-has-to-pay-for-the-mistake-1.6769454" target="_blank" rel="noopener" >加拿大航空赔偿事件</a>,尤其是对敏感的需要正确信息的场景,一旦出现错误的信息往往意味着潜在的损失。</li> </ul> <p>要解决这些问题,那么就需要是用 RAG 来帮助解决。那么,什么是 RAG 呢?</p> <p>Retrieval-Augmented Generation (RAG) 是一种基于检索的生成式模型,它结合了检索式和生成式的优点,可以在生成式模型(Generative Model)中引入外部知识。RAG 模型由两部分组成:一个是检索器,另一个是生成器。检索器负责从知识库中检索相关信息,生成器则负责生成答案。与传统的大语言模型(LLM)仅依靠自身有限的知识进行文本生成不同,RAG 模型能够动态地从海量的外部知识库中检索相关信息,并将这些信息融入到生成过程中,产生更加准确、连贯的输出内容。</p> <p>换句话说,当用户提出一个查询或者需要生成某种类型的文本时,RAG 模型首先会利用一个信息检索子模块,从知识库中检索出与查询相关的信息。然后,RAG 的生成子模块会结合这些检索到的相关信息,生成最终的输出文本。这种结合检索和生成的方式,使 RAG 模型能够弥补传统 LLM 的局限性,提高生成内容的准确性和可靠性。</p> <p>简单点来说,RAG 的基本流程如下:</p> <p><img src="./2024/04/learn-hyde-from-scratch-1/vanilla-rag.webp" width="1527" height="654" srcset="./2024/04/learn-hyde-from-scratch-1/vanilla-rag_hu_585e302af977823d.webp 480w, ./2024/04/learn-hyde-from-scratch-1/vanilla-rag_hu_eae049ef4f12aef4.webp 1024w" loading="lazy" class="gallery-image" data-flex-grow="233" data-flex-basis="560px" ></p> <h2 id="什么是-hyde">什么是 HyDE? </h2><p>但是在是用 RAG 的时候,你往往也会遇到一些问题。</p> <p>让我们设想一个场景:我们有一个知识库,里面存储了大量的文档,我们希望使用 RAG 模型来回答用户的问题。但是,原始的 RAG 只能处理知识库中包含的数据,但是对于知识库中不存在的数据,RAG 无法处理。比较常见的场景包含用户的问答系统,帮助中心,对话系统等。用户通常对你的系统或者专有名词并不了解,他们通常会提出一些根据自己理解或者自己熟悉概念中的名词问题,这些表达方式可能并不在你的知识库中。这时候 RAG 就无法从知识库中检索到相关信息。</p>No GIL Python 的冒险https://www.4async.com/2024/03/adventure-of-no-gil-python/Mon, 18 Mar 2024 10:11:00 +0000https://www.4async.com/2024/03/adventure-of-no-gil-python/<img src="https://www.4async.com/2024/03/adventure-of-no-gil-python/cover.png" alt="Featured image of post No GIL Python 的冒险" /><p>在前几周的时候,Python的<a class="link" href="https://github.com/python/cpython/pull/116338" target="_blank" rel="noopener" >允许禁用 GIL PR</a> 正式合并进入了 Python 3.13 的master分支。这是一个非常重要的 PR,因为它在未来将会对 Python 的并发性能产生非常大的影响。在即将到来的 Python 3.13 中,这个允许禁用 GIL和包含了 Copy and Paste JIT 技术,这些同时都对 Python 的性能产生了非常大的影响。</p> <h2 id="什么是-gil">什么是 GIL </h2><p>GIL,即全局解释器锁,是 Python 语言中一个技术术语。官方实现的 CPython 中包含了 GIL 的实现,同时也是最广泛使用的实现。GIL 的主要目的是在任何时候只允许一个线程执行 Python 字节码,这意味着即使你的程序在多核处理器上运行,也无法实现真正的并行执行。GIL 的存在主要是为了简化 CPython 的内存管理。Python 的对象,如列表、字典等,不是线程安全的,这意味着如果多个线程同时从不同的核心修改同一个对象,可能会导致数据不一致或者程序崩溃。GIL 通过限制同时执行的线程数来避免这种情况。</p> <p>换句话说:如果一个系统线程想要执行 Python 的字节码,必须要获取到 GIL 锁,然后才可以执行 Python 字节码。而如果没有获取到锁,那么线程就会休眠,直到获得信号而被唤醒。为了保证 Python 的效率,Python 也会自动切换线程,比如 IO 阻塞时,或者执行了指定数量的 Python 字节码时。这样就尽量保证了 Python 的效率。当然,你也可以选择手工释放 GIL 锁,比如使用 C/C++/Rust 扩展,或者使用 Cython 等开发高性能扩展时。</p> <p>但是这个 GIL 也是 Python 的一个瓶颈,因为它限制了 Python 的并发性能。在多核处理器上,Python 的并发性能并不是很好。这也是为什么很多人选择使用多进程而不是多线程来提高 Python 的并发性能。在现代的计算机上,多核处理器已经是标配,因此 Python 的并发性能成为了一个非常重要的问题。这也是为什么这么多年来,Python 的 GIL 一直是一个非常热门的话题。</p>使用 Ollama 快速部署本地开源大语言模型https://www.4async.com/2024/02/deploy-local-llm-using-ollama/Wed, 28 Feb 2024 17:39:00 +0000https://www.4async.com/2024/02/deploy-local-llm-using-ollama/<img src="https://www.4async.com/2024/02/deploy-local-llm-using-ollama/cover.png" alt="Featured image of post 使用 Ollama 快速部署本地开源大语言模型" /><p>如果你是第一次开始研究如何使用开源大语言模型(LLM)测试 GenerativeAI 时,一开始所有的信息一股脑在你的眼前,令人望而生畏。互联网上存在着来自许多不同来源的大量碎片信息,使得快速启动项目变得困难。</p> <p>这篇文章的目标是提供一篇简易上手的文章,帮助你使用名为 Ollama 的模型的启动程序在本地设置和运行开源 AI 模型,当然,这些步骤也同样可以让你在你的服务器上运行它。</p> <h2 id="什么是-ollama">什么是 Ollama </h2><p>Ollama 是一款帮助我们在本地机器上运行大型语言模型的工具,它让你在本地测试 LLM 变得更加容易。Ollama 提供了本地命令行和 API 调用多种方式进行交互。如果你是想快速测试,那么 CLI 则是一个非常不错的方式;如果你是想开始一个产品,你也可以选择试用<code>/api/chat</code>进行开发一个应用。</p> <p>Ollama 分为两个部分:一个是运行 LLM 的服务端,另外一个则是可选组件:用于和服务端和 LLM进行交互的 CLI。</p> <h3 id="安装-ollama">安装 Ollama </h3><p><a class="link" href="https://ollama.com/" target="_blank" rel="noopener" >Ollama</a> 官方提供了<a class="link" href="https://ollama.com/download" target="_blank" rel="noopener" >安装包</a>用于 MacOS/Linux/Windows 下载。其中 Windows 支持截止到目前(2024/02/27)为止,还是预览支持,可能存在问题。因此这里我们演示使用 MacOS 安装这个应用。</p> <h4 id="下载安装">下载安装 </h4><p>下载完成后,解压这个 zip 文件即可得到Ollama 的应用程序,你可以把它拖到系统的应用程序文件夹中,双击打开:</p> <p><img src="https://res.craft.do/user/full/1adcdc5f-9fec-35eb-9cb6-9d25a1858140/doc/AC77186B-279D-42E3-94E6-1210D21445CF/7F776093-3862-4581-8084-8B3BA0E87381_2/xtp78WzaqBigmWTxPFklpEhcyiwIjyJ6WBYdsoYkN6oz/Image.png" loading="lazy" alt="image" ></p> <p>如果是第一次打开,会遇到安全提示,选择打开即可。</p> <p><img src="https://res.craft.do/user/full/1adcdc5f-9fec-35eb-9cb6-9d25a1858140/doc/AC77186B-279D-42E3-94E6-1210D21445CF/47F99D14-8F4F-4C2F-AEED-71C8F8894B2C_2/glL5swE2IRE6DUeLCVYDYywm0ksnNLB1FxhstNkbAGwz/2024-02-27%2011.44.33.png" loading="lazy" alt="image" ></p> <p>接下来需要安装 Ollama 的命令行工具,这样你就可以在命令行中访问LLM 了。</p> <p><img src="https://res.craft.do/user/full/1adcdc5f-9fec-35eb-9cb6-9d25a1858140/doc/AC77186B-279D-42E3-94E6-1210D21445CF/043C01CF-0F92-4B75-A566-43CFDAB1E541_2/rOmbRmEOsdZek1JwIw5fDvoMAy6eUli2mziQFEEWTKwz/2024-02-27%2011.46.18.png" loading="lazy" alt="image" ></p> <h4 id="命令行安装">命令行安装 </h4><p>当然,如果你安装了<code>[homebrew](https://brew.sh/)</code>包管理工具,也可以使用下面的方式快速安装 Ollama:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>brew install ollama </span></span></code></pre></div><p>命令行方式安装时,需要额外注意我们无法像图形界面进行后台服务启动。你需要施工执行下面的命令启动Ollama 服务端:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>ollama serve </span></span></code></pre></div><h2 id="试用-ollama">试用 Ollama </h2><p>现在你已经安装完成 Ollama 了,接下来让我们运行一个基础的模型试用一下。Ollama 支持了<a class="link" href="https://ollama.com/library" target="_blank" rel="noopener" >很多的开源模型</a>,包括 Google 最新开源的 <a class="link" href="https://ollama.com/library/gemma" target="_blank" rel="noopener" >Gemma</a>。不过这里我推荐使用<a class="link" href="https://ollama.com/library/mistral" target="_blank" rel="noopener" >Mistral</a> 7B 模型。我们可以使用下面的命令快速安装对应模型:</p>使用子解释器运行Python并行应用https://www.4async.com/2023/11/python-312-sub-interpreters/Thu, 23 Nov 2023 09:39:00 +0000https://www.4async.com/2023/11/python-312-sub-interpreters/<img src="https://www.4async.com/2023/11/python-312-sub-interpreters/cover.webp" alt="Featured image of post 使用子解释器运行Python并行应用" /><blockquote> <p>译者注:最近 Python 3.12 引入了子解释器概念,非常火热,更好的消息是已经<a class="link" href="https://twitter.com/anthonypjshaw/status/1723916190202945761" target="_blank" rel="noopener" >在FastAPI应用成功</a>了,虽然是很简单的那种。因此顺腾摸瓜,找到了作者的<a class="link" href="https://tonybaloney.github.io/posts/sub-interpreter-web-workers.html" target="_blank" rel="noopener" >博客</a>,翻译分享给大家。</p></blockquote> <p>Python 3.12 引入了一个新的 API 用于“子解释器”(sub interpreters),这是 Python 的一种不同的并行执行模型,提供了真正并行处理和多进程处理之间的良好折中,且具有更快的启动时间。在这篇文章中,我将解释什么是子解释器,为什么它对 Python 中的并行代码执行很重要,以及它与其他方法的比较。</p> <h2 id="什么是子解释器">什么是子解释器? </h2><p>Python 的系统架构大致由三部分组成:</p> <ul> <li>一个包含一个或多个解释器的 Python 进程</li> <li>一个包含锁(GIL)和一个或多个 Python 线程的解释器</li> <li>一个包含当前执行代码信息的线程。</li> </ul> <p><img src="./2023/11/python-312-sub-interpreters/interpreter-states-1.png" width="2111" height="1898" srcset="./2023/11/python-312-sub-interpreters/interpreter-states-1_hu_e139c5bca93ea465.png 480w, ./2023/11/python-312-sub-interpreters/interpreter-states-1_hu_a52b647e9e8156e1.png 1024w" loading="lazy" alt="interpreter states" class="gallery-image" data-flex-grow="111" data-flex-basis="266px" ></p> <p>要了解更多关于这方面的信息,你可以阅读我的书<a class="link" href="https://tonybaloney.github.io/#books" target="_blank" rel="noopener" >《CPython 内部实现》</a>中的“并行性和并发性”章节。</p> <p>自 Python 1.5 以来,就有一个 C-API 可以支持多个解释器,但这个功能由于 GIL 的限制而受到严重限制,没有真正实现真正的并行性。因此,运行并行代码最常用的技术(不使用第三方库)是使用 <a class="link" href="https://docs.python.org/3/library/multiprocessing.html" target="_blank" rel="noopener" >multiprocessing 模块</a>。</p> <p>2017 年,CPython 核心开发人员提出改变解释器结构的提议,使它们更好地与拥有它们的 Python 进程隔离,并能够并行操作。实现这一目标的工作相当巨大(6 年后仍未完成),并分为两个 PEP。PEP684 将 GIL 在各个解释器独立开,PEP554 提供了一个创建解释器和在它们之间共享数据的 API。</p> <p>GIL 是“全局解释器锁”,是 Python 进程中的一个锁,意味着在任何时间点 Python 进程中只能执行一条指令,即使它有多个线程。这实际上意味着,即使你在拥有 4 核 CPU 的电脑上同时启动 4 个 Python 线程,也只有一个线程会在任何时候运行。</p>Twirp初相识https://www.4async.com/2023/01/twirp-first-step/Wed, 25 Jan 2023 09:39:00 +0000https://www.4async.com/2023/01/twirp-first-step/<img src="https://www.4async.com/2023/01/twirp-first-step/cover.png" alt="Featured image of post Twirp初相识" /><h2 id="什么是twirp">什么是Twirp? </h2><p>Twirp是Twitch在<a class="link" href="https://blog.twitch.tv/en/2018/01/16/twirp-a-sweet-new-rpc-framework-for-go-5f2febbf35f/" target="_blank" rel="noopener" >2018年</a>开源的RPC框架。正如同他们在发布文章中说的那样,RPC相对于普通的RESTful API更方便设计、组织和维护,让开发者更加专注于业务。但是同样的,在Go社区中重要的gRPC方案严重与HTTP/2绑定,这也成为一个制约其推广的问题:HTTP/2的复杂性其实并不必要;与Go Runtime的割裂也是另外一个问题,导致部分优化难以直接通过升级Go版本在gRPC上显现。</p> <p>Twirp则选择保留了部分好的地方:使用Protobuf这个IDL约束请求/返回类型,这样可以最大化借助Protobuf带来的优势,生成客户端和服务端代码。但是Twirp选择与Go标准库集成,这样可以更好的利用Go本身升级带来的优化。这同时也保证了Twirp本身的简洁性。同时,你也可以很方便的使用cURL等传统工具,借助json请求测试,而不需要手工处理二进制数据。同样的,借助Go标准库,未来Twirp可以更好的升级成HTTP/3而不是像gRPC一样等待上游更新。当然如果你更倾向于使用gRPC相关的实践,那么<a class="link" href="https://buf.build/blog/connect-a-better-grpc" target="_blank" rel="noopener" >connect-go</a>可能是你的另外一个不错选择。</p> <p>当然,如果说缺点,Twirp并不完美:小众的社区,缺少生态,缺少相关信息内容等等。不过这些仍旧是瑕不掩瑜。毕竟实现一个相关的功能其实并不那么复杂。</p> <h2 id="如何使用twirp">如何使用Twirp </h2><p>Twirp虽然官网比较简单,甚至社区也不是很大的样子,但是基本上需求的数据基本都可以在官网上找到入口。但是这也有个问题,导致整个流程对新手并不友好,有比较高的上手门槛。接下来的内容主要是完善这部分的内容,方便新手用户使用。</p> <h3 id="安装protobuf相关工具">安装Protobuf相关工具 </h3><p>由于Twirp同样使用Protobuf,我们需要使用相关工具。首先是Protobuf,接下来是一些protoc-gen工具:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Bash" data-lang="Bash"><span style="display:flex;"><span>brew install protobuf <span style="color:#75715e"># Mac Only</span> </span></span><span style="display:flex;"><span>go install google.golang.org/protobuf/cmd/protoc-gen-go@latest </span></span><span style="display:flex;"><span>go install github.com/twitchtv/twirp/protoc-gen-twirp@latest </span></span></code></pre></div><h4 id="可选项buf">可选项:Buf </h4><p>Buf是一个Protobuf管理工具,帮助你实现<a class="link" href="https://99designs.com.au/blog/engineering/schema-driven-development/" target="_blank" rel="noopener" >Schema Driven Development</a>实践。它提供了一个CLI管理工具(支持lint,生成和破坏性检查等功能)和类似注册中心机制的BSR(Buf Schema Registry),你可以在这里管理你的Schema版本和引用其他公开服务的Schema。不使用Buf并不会带来功能缺失,并且Buf提供了<a class="link" href="https://buf.build/pricing" target="_blank" rel="noopener" >付费SaaS服务</a>(测试期间免费),可以根据你的情况选择是否使用。</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Bash" data-lang="Bash"><span style="display:flex;"><span>brew install bufbuild/buf/buf <span style="color:#75715e"># Mac Only</span> </span></span></code></pre></div><h4 id="可选项taskfile">可选项:Taskfile </h4><p><a class="link" href="https://taskfile.dev/" target="_blank" rel="noopener" >Taskfile</a>是我常用来替代Makefile的工具。这并不是必须的工具,你同样可以使用手工执行命令行和Makefile命令进行。事实上,使用Makefile其实可以更好的在Jenkins之类的pipeline里执行,但是对Github Action等现代pipeline而言,区别并不大。</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Bash" data-lang="Bash"><span style="display:flex;"><span>brew install go-task/tap/go-task <span style="color:#75715e"># Mac Only</span> </span></span></code></pre></div><h3 id="生成项目文件">生成项目文件 </h3><p>这里我们使用一个简单的Greeter程序演示使用。假设我们已经存在了一个Go的空项目,那么我们接下来需要创建对应的目录和文件。按照<a class="link" href="https://twitchtv.github.io/twirp/docs/best_practices.html#folderpackage-structure" target="_blank" rel="noopener" >官方的建议</a>,我们可以使用如下结构创建我们的项目,你可以在Github上查看<a class="link" href="https://github.com/ipfans/twirp-demo" target="_blank" rel="noopener" >完整的代码</a>:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Bash" data-lang="Bash"><span style="display:flex;"><span>$ tree . </span></span><span style="display:flex;"><span>. </span></span><span style="display:flex;"><span>├── README.md </span></span><span style="display:flex;"><span>├── Taskfile.yaml </span></span><span style="display:flex;"><span>├── buf.gen-ts.yaml </span></span><span style="display:flex;"><span>├── buf.gen.yaml </span></span><span style="display:flex;"><span>├── buf.yaml </span></span><span style="display:flex;"><span>├── build </span></span><span style="display:flex;"><span>├── client </span></span><span style="display:flex;"><span>│ ├── package.json </span></span><span style="display:flex;"><span>│ ├── pnpm-lock.yaml </span></span><span style="display:flex;"><span>│ └── src </span></span><span style="display:flex;"><span>│ └── protoc-gen-twirp-es.ts </span></span><span style="display:flex;"><span>├── cmd </span></span><span style="display:flex;"><span>│ └── greeter </span></span><span style="display:flex;"><span>│ ├── main.go </span></span><span style="display:flex;"><span>│ └── main_test.go </span></span><span style="display:flex;"><span>├── go.mod </span></span><span style="display:flex;"><span>├── go.sum </span></span><span style="display:flex;"><span>├── internal </span></span><span style="display:flex;"><span>│ └── greetersvc </span></span><span style="display:flex;"><span>│ └── service.go </span></span><span style="display:flex;"><span>└── rpc </span></span><span style="display:flex;"><span> └── greeter </span></span><span style="display:flex;"><span> └── v1 </span></span><span style="display:flex;"><span> └── service.proto </span></span></code></pre></div><h2 id="编写服务端">编写服务端 </h2><p>我们先看一下greeter的服务定义:</p>Twirp基本概念:Hooks和Interceptorshttps://www.4async.com/2023/01/twirp-hooks-and-interceptors/Wed, 25 Jan 2023 09:39:00 +0000https://www.4async.com/2023/01/twirp-hooks-and-interceptors/<img src="https://www.4async.com/2023/01/twirp-hooks-and-interceptors/cover.png" alt="Featured image of post Twirp基本概念:Hooks和Interceptors" /><p>Twirp做了足够多的抽象工作,使得我们可以在不同的层次上进行扩展。在最基础的使用层面上,你可以像<code>net/http</code>一样去简单的使用。但是往往这样也不能满足我们实际项目中的需求在这篇文章中,我们将介绍Twirp的Hooks和Interceptors基本概念,以及如何使用这些Twirp的扩展机制。</p> <h2 id=""> </h2>去年的一点小工作(1):从BFF谈起https://www.4async.com/2023/01/something-happend-in-2022-1/Sat, 21 Jan 2023 17:55:39 +0000https://www.4async.com/2023/01/something-happend-in-2022-1/<img src="https://www.4async.com/2023/01/something-happend-in-2022-1/cover.jpg" alt="Featured image of post 去年的一点小工作(1):从BFF谈起" /><p>前年底,之前创业的公司关掉正式回归了打工生活。现在在一家SaaS公司做一些新业务开发和架构的工作。这个系列的文章也是想整理一下2022年的一些小成果,有一些内容和相关背景可能因为各种原因无法描述更细节的内容,也请各位见谅。</p> <h1 id="后端和前端的配合我们遇到了什么问题">后端和前端的配合,我们遇到了什么问题? </h1>Byebye 2022, Hello 2023https://www.4async.com/2023/01/byebye-2022-hello-2023/Sun, 01 Jan 2023 17:53:39 +0000https://www.4async.com/2023/01/byebye-2022-hello-2023/<img src="https://www.4async.com/2023/01/byebye-2022-hello-2023/2022-2023.jpg" alt="Featured image of post Byebye 2022, Hello 2023" />给hugo添加mermaid支持https://www.4async.com/2022/12/add-mermaid-support-for-hugo/Mon, 26 Dec 2022 17:48:00 +0000https://www.4async.com/2022/12/add-mermaid-support-for-hugo/<img src="https://www.4async.com/2022/12/add-mermaid-support-for-hugo/gohugo.png" alt="Featured image of post 给hugo添加mermaid支持" /><p>发现2022年居然一篇博客都没写,一方面是疫情原因实在太糟心,另外一方面是环境使然,2022年一年都在适应新环境和拼命的推动项目前行。立个Flag,保证明年至少一个季度有一篇文章吧。</p> <p>这篇文章是为了提示如何在hugo中添加mermaid支持,mermaid是一个流程图的工具,可以在markdown中直接使用,非常方便。其实在hugo官方文档中已经有了说明,但是我在使用的时候发现对新人不是特别友好,所以这里记录一下,顺便水一个。</p>一些实用工具列表https://www.4async.com/2021/11/awesome-toolkit/Mon, 29 Nov 2021 17:48:00 +0000https://www.4async.com/2021/11/awesome-toolkit/<img src="https://www.4async.com/2021/11/awesome-toolkit/toolkit.png" alt="Featured image of post 一些实用工具列表" /><p>一些在工作中经常使用的一些工具。如果有什么推荐的,也欢迎在评论中提供。这个列表后续会持续更新</p> <h2 id="http工具">HTTP工具 </h2><ul> <li><a class="link" href="https://curlie.io" target="_blank" rel="noopener" ><strong>curlie</strong></a> - httpie-like 工具,底层是curl</li> <li><a class="link" href="https://github.com/go-acme/lego" target="_blank" rel="noopener" ><strong>lego</strong></a> - Let&rsquo;s Encrypt证书工具</li> <li><a class="link" href="https://github.com/FiloSottile/mkcert" target="_blank" rel="noopener" ><strong>mkcert</strong></a> - 方便导入本地证书</li> <li><a class="link" href="https://paw.cloud" target="_blank" rel="noopener" ><strong>paw.cloud</strong></a> - 原生的macOS HTTP调试工具,现在每年都会免费送,有兴趣关注一下</li> </ul> <h2 id="编译工具">编译工具 </h2><ul> <li><a class="link" href="https://taskfile.dev" target="_blank" rel="noopener" ><strong>go-task</strong></a> - 我用来替代Makefile,并无什么特殊必要,主要是不想写Makefile</li> </ul> <h2 id="代码质量">代码质量 </h2><ul> <li><a class="link" href="https://github.com/golangci/golangci-lint" target="_blank" rel="noopener" ><strong>golangci-lint</strong></a> - 感觉无需介绍了,集成了很多实用工具,重复的就不列举了</li> <li><a class="link" href="https://pre-commit.com" target="_blank" rel="noopener" ><strong>pre-commit</strong></a> - 提交前检查代码质量,比如代码风格,缩进,空格等等</li> <li><a class="link" href="https://github.com/boyter/dcd" target="_blank" rel="noopener" ><strong>dcd</strong></a> - 查找代码中的重复代码</li> </ul> <h2 id="代码统计">代码统计 </h2><ul> <li><a class="link" href="https://github.com/boyter/scc" target="_blank" rel="noopener" ><strong>scc</strong></a> - 高性能统计代码行数</li> </ul> <h2 id="图表工具">图表工具 </h2><ul> <li><a class="link" href="https://github.com/blushft/go-diagrams" target="_blank" rel="noopener" ><strong>go-diagrams</strong></a> - 使用Go语言描述系统架构图</li> <li><a class="link" href="https://github.com/k1LoW/ndiag" target="_blank" rel="noopener" ><strong>ndiag</strong></a> - 如果不想用Go描述,也可以选择用YAML描述系统架构</li> <li><a class="link" href="https://github.com/lucasepe/draft" target="_blank" rel="noopener" ><strong>draft</strong></a> - 另外一个用YAML描述的工具,风格不一样</li> <li><a class="link" href="https://github.com/mkimuram/k8sviz" target="_blank" rel="noopener" ><strong>k8sviz</strong></a> - 你也可以从现成的K8s环境中生成系统架构图</li> <li><a class="link" href="https://github.com/storj/archview" target="_blank" rel="noopener" ><strong>archview</strong></a> - 通过代码中注释生成应用内部分层结构</li> <li><a class="link" href="https://github.com/bykof/go-plantuml" target="_blank" rel="noopener" ><strong>go-plantuml</strong></a> - 根据Go代码生成结构体的PlantUML图</li> <li><a class="link" href="https://github.com/jfeliu007/goplantuml" target="_blank" rel="noopener" ><strong>goplantuml</strong></a> - 另外一种生成PlantUML的工具</li> <li><a class="link" href="https://github.com/gmarik/go-erd" target="_blank" rel="noopener" ><strong>go-erd</strong></a> - 不想用PlantUML也可以换这种风格</li> <li><a class="link" href="https://asciiflow.com" target="_blank" rel="noopener" ><strong>asciiflow</strong></a> - 可以画ASCII图,ASCII图好处是可以放在代码里,如果你愿意的话</li> <li><a class="link" href="https://textart.io/sequence" target="_blank" rel="noopener" ><strong>sequence</strong></a> - 嫌弃asciiflow比较原始,做时序图的时候可以用这个</li> <li><a class="link" href="https://mermaid-js.github.io/mermaid-live-editor/" target="_blank" rel="noopener" ><strong>mermaid-js</strong></a> - 方便集成在网页中,也可以导出成图片</li> <li><a class="link" href="https://kroki.io/" target="_blank" rel="noopener" ><strong>kroki</strong></a> - 上面没提到的图类型的生成?看看这个</li> </ul> <h2 id="iac">IaC </h2><ul> <li><a class="link" href="https://www.pulumi.com/" target="_blank" rel="noopener" ><strong>pulumi</strong></a> - Terraform业界比较常用,不过要学习HCL比较蛋疼,我个人比较喜欢pulumi,可以选择自己的习惯的语言,tf-cdk目前还比较初级。</li> </ul>Go 1.17 泛型尝鲜https://www.4async.com/2021/08/golang-117-generics/Tue, 17 Aug 2021 17:48:00 +0000https://www.4async.com/2021/08/golang-117-generics/<img src="https://www.4async.com/2021/08/golang-117-generics/go.png" alt="Featured image of post Go 1.17 泛型尝鲜" /><p>今天,Go的1.17版本终于正式发布,除了带来各种优化和新功能外,1.17正式在程序中提供了尝鲜的泛型支持,这一功能也是为1.18版本泛型正式实装做铺垫。意味着在6个月后,我们就可以正式使用泛型开发了。那在Go 1.18正式实装之前,我们在1.17版本中先尝鲜一下泛型的支持吧。</p> <h2 id="泛型有什么作用">泛型有什么作用? </h2><p>在使用Go没有泛型之前我们怎么实现针对多类型的逻辑实现的呢?有很多方法,比如说使用<code>interface{}</code>作为变量类型参数,在内部通过类型判断进入对应的处理逻辑;将类型转化为特定表现的鸭子类型,通过接口定义的方法实现逻辑整合;还有人专门编写了Go的<a class="link" href="https://twitter.com/yogthos/status/883058510275149826" target="_blank" rel="noopener" >函数代码生成工具</a>,通过批量生成不同类型的相同实现函数代替手工实现等等。这些方法多多少少存在一些问题:使用了<code>interface{}</code>作为参数意味着放弃了编译时检查,作为强类型语言的一个优势就被抹掉了。同样,无论使用代码生成还是手工书写,一旦出现问题,意味着这些方法都需要重复生成或者进行批量修改,工作量反而变得更多了。</p> <p>在Go中引入泛型会给程序开发带来很多好处:通过泛型,可以针对多种类型编写一次代码,大大节省了编码时间。你可以充分应用编译器的编译检查,保证程序变量类型的可靠性。借助泛型,你可以减少代码的重复度,也不会出现一处出现问题需要修改多处地方的尴尬问题。这也让很多测试工作变得更简单,借助类型安全,你甚至可以少考虑很多的边缘情况。</p> <p>Go语言官方有详细的泛型提案文档可以在<a class="link" href="https://go.googlesource.com/proposal/&#43;/refs/heads/master/design/15292-generics.md" target="_blank" rel="noopener" >这里</a>和<a class="link" href="https://go.googlesource.com/proposal/&#43;/refs/heads/master/design/15292/2013-12-type-params.md" target="_blank" rel="noopener" >这里</a>查看详情。</p> <h2 id="如何使用泛型">如何使用泛型 </h2><p>前面理论我们仅仅只做介绍,这次尝鲜还是以实践为主。让我们先从一个小例子开始。</p> <h3 id="从简单的例子开始">从简单的例子开始 </h3><p>让我们先从一个最简单的例子开始:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> ( </span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#34;fmt&#34;</span> </span></span><span style="display:flex;"><span>) </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Addable</span> <span style="color:#66d9ef">interface</span> { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">type</span> <span style="color:#66d9ef">int</span>, <span style="color:#66d9ef">int8</span>, <span style="color:#66d9ef">int16</span>, <span style="color:#66d9ef">int32</span>, <span style="color:#66d9ef">int64</span>, </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">uint</span>, <span style="color:#66d9ef">uint8</span>, <span style="color:#66d9ef">uint16</span>, <span style="color:#66d9ef">uint32</span>, <span style="color:#66d9ef">uint64</span>, <span style="color:#66d9ef">uintptr</span>, </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">float32</span>, <span style="color:#66d9ef">float64</span>, <span style="color:#66d9ef">complex64</span>, <span style="color:#66d9ef">complex128</span>, </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">string</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">add</span>[<span style="color:#a6e22e">T</span> <span style="color:#a6e22e">Addable</span>](<span style="color:#a6e22e">a</span>, <span style="color:#a6e22e">b</span> <span style="color:#a6e22e">T</span>) <span style="color:#a6e22e">T</span> { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">a</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">b</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() { </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">add</span>(<span style="color:#ae81ff">1</span>,<span style="color:#ae81ff">2</span>)) </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">add</span>(<span style="color:#e6db74">&#34;1&#34;</span>, <span style="color:#e6db74">&#34;2&#34;</span>)) </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><p>这个函数可以实现任何需要使用<code>+</code>符号进行运算的类型,我们通过定义<code>Addable</code>类型,枚举了所有可能可以使用<code>add</code>方法的所有的类型。比如我们在<code>main</code>函数中就使用了<code>int</code>和<code>string</code>两种不同类型。</p> <p>但是如果这时我们使用简单的<code>go run</code>命令运行,会发现提示语法错误:</p> <pre tabindex="0"><code>$ go version go version go1.17 darwin/arm64 $ go run ~/main.go # command-line-arguments ../main.go:8:2: syntax error: unexpected type, expecting method or interface name ../main.go:15:6: missing function body ../main.go:15:9: syntax error: unexpected [, expecting ( </code></pre><p>因为在Go 1.17中,泛型并未默认开启,你需要定义<code>gcflags</code>方式启用泛型:</p>Go Web应用中常见的反模式https://www.4async.com/2021/08/common-anti-patterns-in-go-web-applications/Fri, 13 Aug 2021 14:09:00 +0000https://www.4async.com/2021/08/common-anti-patterns-in-go-web-applications/<img src="https://www.4async.com/2020/02/2020-02-16-moving-towards-domain-driven-design-in-go/cover_huea63e6370dcf0c375755d886a5d0b9c6_97708_1600x0_resize_q75_box.jpg" alt="Featured image of post Go Web应用中常见的反模式" /><blockquote> <p>作者:Miłosz Smółka <br> 译者:Kevin <br> 原文地址:https://threedots.tech/post/common-anti-patterns-in-go-web-applications/</p></blockquote> <p>在我职业生涯的某个阶段,我对我所构建的软件不再感到兴奋。</p> <p>我最喜欢的工作内容是底层的细节和复杂的算法。在转到面向用户的应用开发之后,这些内容基本消失了。编程似乎是利用现有的库和工具把数据从一处移至另一处。到目前为止,我所学到的关于软件的知识不再那么有用了。</p> <p>让我们面对现实吧:大多数Web应用无法解决棘手的技术挑战。他们需要做到的是正确的对产品进行建模,并且比竞争对手更快的改进产品。</p> <p>这起初看起来似乎是那么的无聊,但是你很快会意识到实现这个目标比听起来要难。这是一项完全不同的挑战。即使它们技术上实现并没有那么复杂,但时解决它们会对产品产生巨大影响并且让人获得满足。</p> <p><strong>Web应用面临的最大挑战不是变成了一个无法维护的屎山,而是会减慢你的速度,让你的业务最终失败。</strong></p> <p>这是他们如何在Go中发生和我是如何避免他们的。</p> <h2 id="松耦合是关键">松耦合是关键 </h2><p>应用难以维护的一个重要原因是强耦合。</p> <p><strong>在强耦合应用中,任何你尝试触动的东西都有可能产生意想不到的副作用</strong>。每次重构的尝试都会发现新的问题。最终,你决定字号从头重写整个项目。在一个快速增长的产品中,你是不可能冻结所有的开发任务去完成重写已经构建的应用的。而且你不能保证这次你把所有事都完成好。</p> <p>相比之下,<strong>松耦合应用保持了清晰的边界</strong>。他们允许更换一些损坏的部分不影响项目的其他部分。它们更容易构建和维护。但是,为什么他们如此罕见呢?</p> <p>微服务许诺了松耦合时实践,但是我们现在已经过了他们的炒作年代,而难以维护的应用仍旧存在。有些时候这反而变得更糟糕了:我们落入了<a class="link" href="https://threedots.tech/post/microservices-or-monolith-its-detail/" target="_blank" rel="noopener" >分布式单体</a>的陷阱,处理和之前相同的问题,而且还增加了网络开销。</p> <p><img src="https://threedots.tech/post/common-anti-patterns-in-go-web-applications/microservices.png" loading="lazy" alt="从强耦合单体应用到分布式单体" ></p> <blockquote> <p>❌ 反模式:分布式单体 <br> 在你了解边界之前,不要将你的应用切分成为微服务。</p></blockquote> <p>微服务并不会降低耦合,<strong>因为拆分服务的次数并不重要。重要的是如何连接各个服务</strong>。</p> <p><img src="https://threedots.tech/post/common-anti-patterns-in-go-web-applications/monolith.png" loading="lazy" alt="从模块化单体应用到松耦合微服务" ></p> <blockquote> <p>✅ 策略:松耦合 <br> 以实现松耦合的模块为目标。如何部署它们(作为模块化单体应用或微服务)是一个实现细节。</p></blockquote> <h2 id="dry引入了耦合">DRY引入了耦合 </h2><p>强耦合十分常见,因为我们很早就学到了不要重复自己(Don&rsquo;t Repeat Yourself, DRY)原则。</p> <p>简短的规则很容易被大家记住,但是简短的三个单词很难概括所有的细节。《程序员修炼之道: 从小工到专家》这本书提供了一个更长的版本:</p> <blockquote> <p>每条知识在系统中都必须有一个单一的、明确的、权威的表述。</p></blockquote> <p>&ldquo;每一条知识&quot;这个说法相当极端。大多数编程困境的答案是看情况而定,DRY也不例外。</p> <p><strong>当你让两个事物使用相同抽象的时候,你就引入了耦合。如果你严格遵循DRY原则,你就需要在这个抽象之前增加抽象。</strong></p> <p><img src="https://threedots.tech/post/common-anti-patterns-in-go-web-applications/coupling.png" loading="lazy" ></p> <h2 id="在go中保持dry">在Go中保持DRY </h2><p>相比于其他现代语言,Go是清晰的,缺少很多特性,没有太多的语法糖来隐藏复杂性。</p> <p>我们习惯了捷径,所以一开始很难接受Go的冗长。就像我们已经开发出一种去寻找一种更加聪明的编写代码的方式的本能。</p> <p>最典型的例子就是错误处理。如果你有编写Go的经验,你会觉得下面的代码片段很自然</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> { </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span> </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><p>但是对新手而言,一遍又一遍的重复这三行就是似乎在破坏DRY原则。他们经常想办法来规避这种样板方法,但是却没有什么好的结果。</p> <p>最终,大家都接受了Go的工作方式。<strong>它让你重复你自己,不过这并不是DRY告诉你的你要避免重复。</strong></p> <h2 id="单一数据模型带来的应用耦合">单一数据模型带来的应用耦合 </h2><p><strong>Go中有一个特性引入了强耦合,但会让你认为你自己在遵循DRY原则。这就是在一个结构体中使用多个标签。</strong> 这似乎是一个好主意,因为我们经常对不同的事物使用相似的模型。</p> <p>这里有一个流行的方式保存单个<code>User</code>模型的方法:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">User</span> <span style="color:#66d9ef">struct</span> { </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ID</span> <span style="color:#66d9ef">int</span> <span style="color:#e6db74">`json:&#34;id&#34; gorm:&#34;autoIncrement primaryKey&#34;`</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">FirstName</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;first_name&#34; validate:&#34;required_without=LastName&#34;`</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">LastName</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;last_name&#34; validate:&#34;required_without=FirstName&#34;`</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">DisplayName</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;display_name&#34;`</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Email</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;email,omitempty&#34; gorm:&#34;-&#34;`</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Emails</span> []<span style="color:#a6e22e">Email</span> <span style="color:#e6db74">`json:&#34;emails&#34; validate:&#34;required,dive&#34; gorm:&#34;constraint:OnDelete:CASCADE&#34;`</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">PasswordHash</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;-&#34;`</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">LastIP</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;-&#34;`</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">CreatedAt</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:&#34;-&#34;`</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UpdatedAt</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#e6db74">`json:&#34;-&#34;`</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Email</span> <span style="color:#66d9ef">struct</span> { </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ID</span> <span style="color:#66d9ef">int</span> <span style="color:#e6db74">`json:&#34;-&#34; gorm:&#34;primaryKey&#34;`</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Address</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;address&#34; validate:&#34;required,email&#34; gorm:&#34;size:256;uniqueIndex&#34;`</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Primary</span> <span style="color:#66d9ef">bool</span> <span style="color:#e6db74">`json:&#34;primary&#34;`</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">UserID</span> <span style="color:#66d9ef">int</span> <span style="color:#e6db74">`json:&#34;-&#34;`</span> </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><p>完整代码:<a class="link" href="https://github.com/ThreeDotsLabs/go-web-app-antipatterns/blob/ab94d47350756716a7fed42b7ae546c51c11406a/01-coupling/01-tightly-coupled/internal/user.go#L9" target="_blank" rel="noopener" >github.com/ThreeDotsLabs/go-web-app-antipatterns/01-coupling/01-tightly-coupled/internal/user.go</a></p>什么是事件建模Event Modeling?https://www.4async.com/2021/07/introducing-event-modeling/Tue, 06 Jul 2021 11:41:00 +0000https://www.4async.com/2021/07/introducing-event-modeling/<img src="https://www.4async.com/2021/07/introducing-event-modeling/event-modeling-tutorial.jpg" alt="Featured image of post 什么是事件建模Event Modeling?" /><h2 id="基本概念">基本概念 </h2><p>事件建模(Event Modeling)是一种描述系统的方法,展示信息如何随时间变化的例子。具体来说,这种方式省略了瞬息万变的细节,而着眼于在任何特定的时间点上的持久化存储和用户所见数据的变化。这些时间轴上的事件,构成了对系统的描述。</p> <p>近年来,很多系统使用事件通过事件存储数据库或者使用特定方式使用常规数据库构建了状态和信息传播的模块。然而,大多数方法仍然依赖于通过SQL数据库、文档数据库或者其他技术实现严格意义上的当前时间点信息的视图。</p> <p>对很多系统而言,特别是对于非小型系统而言,随着系统复杂性的增加,变更成本将会随着时间的推移难度指数级上升。与现有的设计和建模方式对比,事件建模可以在短时间内创建一个基础蓝图,将返工工作量降到最低。</p> <h3 id="从过去谈起">从过去谈起 </h3><p>讲故事自古以来就是人类能够将知识传递给后代的方法,它在很大程度上依赖于我们如何存储记忆-无论是逻辑的、视觉的、听觉的还是其他的。这一点很重要,因为这与信息系统的构建方式有相似之处。 用具体的例子说明某物应该如何工作是一种常见的方式。这种方式可以在软件开发的成功实践中看到,如行为驱动开发。这种方式很有效,因为我们通过故事来沟通更有效。将它和讲故事联系在一起,是一种保持社会信息的方式。我们的大脑是为它而建的,而不是为流程图和其他格式而建的。 而事件建模模型就是遵循这种讲故事模式而建立的产品建模方式。</p> <h2 id="事件建模模式">事件建模模式 </h2><p><img src="https://eventmodeling.org/posts/what-is-event-modeling/blueprint_large.jpg" loading="lazy" ></p> <p>时间线是最好描述故事主线的方式,对我们的系统而言,时间线也是描述我们系统核心部分概念的重要组成部分。我们可以通过在一条时间线上,系统从开始到结束,在没有分支情况下应该做什么方式展示我们系统的一部分功能。这就是一个典型的事件模型的组成。我们可以用这种方式跟踪所有UI界面中字段值如何存储和如何展示的。比如在上面的示例图中,我们使用了3种不同模块的内容和传统的线框模型就展示了整个系统的模型。但是简单性是我们重要的一个目标,因此我们只依赖于4种模式构建这种模型图。</p> <h3 id="保持简单性">保持简单性 </h3><p>当我们想采用某些做法或流程来帮助彼此理解和沟通时,它与个人为熟练掌握这些方法而进行的学习量成反比。换句话说,如果我们可以更快的掌握一个名叫X的方法时,我们就可以更好的通过这种方式进行知识分享和互动;反之,无论这种方法多么好,昂贵的学习成本总会搞砸一切。</p> <blockquote> <p>当一本书是团队中的必读书目时,每个人都会说他们读过;但事实上只有一半的人会真正读过;这些真正读过的人中一半的人会声称他们理解了这本书;而这些声称理解的人中只有一半的人真正的理解了这本书;而这些真正理解了这本书的人只有一半的人能够使用它。</p></blockquote> <p>这就是为什么使用3个模块和基于2个想法的4种模式进行事件建模。因为这只需要几分钟就可以将所有的东西向所有人解释清楚。其他的学习则可以在实践中进行。即便理解出现了不足和错误,也可以很快在实践中得到纠正。</p> <h3 id="事件">事件 </h3><p>假设我们想为连锁酒店设计一个酒店网站,让我们的客户可以在线预订房间,并让我们安排清洁和任何其他酒店问题。 我们可以显示在该业务的年度时间线上存储了哪些事件。 我们可以假装我们已经有了这个系统,然后问自己随着时间的推移存储了哪些事件。</p> <h3 id="线框图">线框图 </h3><p>让我们看一下在图片的最上面的部分的第一个模块。为了让讲故事这个事情更加可视化,我们可以在顶部显示功能的线框图或者网页模拟图。这也可以被具象化为具体的泳道图,以方便不同的人(也可以是系统)与我们的系统进行互动。这里一些自动化的内容可以用齿轮表示,同时说明系统正在做什么。通过这种方式,我们可以非常容易的展示出系统需要实现的功能列表,执行流程和项目完成标记。这里的图是示例了一个酒店的预订、支付和通知系统的过程,我们可以重点关注一下所有相关高亮显示的内容。</p> <p><img src="https://eventmodeling.org/posts/what-is-event-modeling/innovate_large.jpg" loading="lazy" ></p> <p>借助这个模块,我们可以很方便的和设计师一起沟通设计系统,当然,这里需要注意在设计中,两个重要的内容需要添加到整个设计中:用户所拥有的权限和用户可以获取的信息。</p> <h3 id="命令">命令 </h3><p>大多数信息系统必须给用户一种影响系统的状态的能力,而这种能力就是命令。在我们的例子中,我们必须允许房间预订改变系统状态,这样我们就不会发生超额预订情况。当那个人在未来的预订日期到达时,他们就有一个为他们准备的房间。</p> <p>改变系统状态的意图会被封装在一个命令中。相对于简单地将表单数据保存到数据库中的一个表中,这可以让我们以非技术性的方式来显示意图,同时允许任何实现 - 尽管我们可以看到某些方法更具优势。</p> <p><img src="https://eventmodeling.org/posts/what-is-event-modeling/empower_large.jpg" loading="lazy" ></p> <p>从UI和UX的角度来看,这就是一个&quot;命令响应式用户界面&quot;,对帮助制作可组合的UI大有帮助。使用这种模式,从技术和商业的角度来看,交易的界限就更清楚了。以酒店入住为例,酒店的客人要么登记成功,要么没有。</p> <p>当命令成功的前提条件有细微的差别时,它们会在&quot;Given-When-Then&quot;风格的描述方式中进行阐述。这种方式也是行为测试模式惯用的描述方式,也是一种成功的讲述故事的方式。实际执行过程中,可能会有几个这样的故事来说明一个命令如何能成功执行和不能成功执行。</p> <p>这里我们可以用一个例子来描述一下:</p> <blockquote> <p>Given:我们已经注册并添加了一个支付方式</p></blockquote> <blockquote> <p>When:我们试图预订一个房间</p></blockquote> <blockquote> <p>Then:一个房间被预订了</p></blockquote> <p>这种描述方式也通常被叫做“安排、行动、断言”,在UI/UX的世界中,也被称为“情景、统计、价值”。 在图中我们也可以发现,所有的命令都是用蓝色进行标记的。</p> <h3 id="视图或者叫读模型">视图(或者叫读模型) </h3><p>任何信息系统的一个重要能力是将系统中保存的状态告知用户。我们的酒店客人需要知道他们感兴趣的某些类型的房间在哪一天可以入住。这通常有很多种情况,需要支持信息系统的多个模型。</p> <p><img src="https://eventmodeling.org/posts/what-is-event-modeling/inform_large.jpg" loading="lazy" ></p> <p>随着这些新事件的存储,系统中的视图也会一直变化。在我们的酒店系统中,这个日历视图随着影响库存的新事件的发生而被更新。其他视图中清洁团队可以在客户离店事件存储后在其他视图中看到房间已经可以被清理了。</p> <p>指定视图的行为方式与我们指定接受命令的方式非常相似,但有一处不同。视图是被动的,并且不能在事件被存储到系统中之后撤销事件。</p> <p>举个例子:</p> <blockquote> <p>Given:酒店设置了12间海景房,海景房从4月4日到12日被预订</p></blockquote> <blockquote> <p>Then:日历上应该显示除4月4日到12日以外的所有海景房的日期</p></blockquote> <p>从上面图中我们也可以注意到,所有的读都是用绿色进行标记的。</p> <h3 id="集成">集成 </h3><p>我们刚刚介绍了描述大多数系统所需的 4 种模式中的前 2 种模式。 系统可以从其他系统获取信息并且将信息发送到其他系统。 强迫这 2 个模式成为前 2 个模式的扩展并共享相同的空间是很诱人的选择。 然而这会让交流变得更加困难,因为它们没有人类可见的方面,并且需要一些更高级别的模式。</p> <h3 id="翻译">翻译 </h3><p>当我们有一个为我们提供信息的外部系统时,将这些信息转换成我们自己系统中的更熟悉的形式会很有帮助。 在我们的酒店系统中,如果选择让我们的清洁人员反应更加灵敏,我们可以从客人的 GPS 坐标中获取事件。 我们不想使用经度和纬度对作为事件来指定我们系统中的先决条件。 我们宁愿选择对我们有意义的活动,例如“客人离开酒店”、“客人回到酒店房间”。</p>构建属于你自己的dapr绑定组件https://www.4async.com/2021/05/building-your-own-dapr-binding/Sat, 15 May 2021 18:38:00 +0000https://www.4async.com/2021/05/building-your-own-dapr-binding/<img src="https://vip1.loli.io/2022/03/26/uJdVWNeXC6Z3Hq8.png" alt="Featured image of post 构建属于你自己的dapr绑定组件" /><p>在<a class="link" href="./2021/05/2021-05-08-building-your-own-dapr-service-discovery" >上一篇文章</a>中,吐槽了拖延症的危害,因此这次我来分享一下我最新推送到dapr的最新的一个新的绑定组件,通过这个来看一下如何实现自己的绑定组件。</p> <p>文中提到的PR可以在 <a class="link" href="https://github.com/dapr/components-contrib/pull/872" target="_blank" rel="noopener" >dapr/components-contrib#872</a> 查看对应的具体代码。</p> <h2 id="什么是-dapr-的绑定组件">什么是 dapr 的绑定组件? </h2><p>在dapr中,绑定是用于使用外部系统功能(比如事件或者接口)的扩展组件。它的优势在于:</p> <ul> <li>免除连接到消息传递系统(如队列和消息总线)并进行轮询的复杂性;</li> <li>聚焦于业务逻辑,而不是如何与系统交互的实现细节;</li> <li>使代码不受 SDK 或库的跟踪;</li> <li>处理重试和故障恢复;</li> <li>在运行时在绑定之间切换;</li> <li>构建具有特定于环境的绑定的可移植应用程序,不需要进行代码更改;</li> </ul> <p>在官方文档中,也提到了一个具体的例子:以twilio发送短信为例,一般开发过程中应用程序需要依赖Twilio SDK才可以实现功能,但是借助绑定组件,你可以将SDK的绑定转移至dapr程序领域内,在本身应用程序中不再绑定对应的SDK,不用担心未来SDK过期或者变更带来的重复工作(仅需要更新dapr即可)。</p> <p>根据订阅的进出方向,绑定组件也分为输入绑定和输出绑定。这些绑定均是通过yaml文件描述类型和元数据,通过HTTP/gRPC进行调用。</p> <h2 id="如何实现自己的绑定组件">如何实现自己的绑定组件? </h2><p>官方例子中提供了一个基础的介绍,上一节中我们也提到了在程序中,根据进出方向可以把绑定组件分为输出绑定和输入绑定。你可以通过官方教程中的例子提供查看:</p> <p><img src="https://raw.githubusercontent.com/dapr/quickstarts/master/bindings/img/Bindings_Standalone.png" loading="lazy" ></p> <p>在这个例子用,你可以看到,根据方向吧dapr发布消息到Kafka作为输出组件,把Kafka读取消息到dapr作为输入组件。</p> <p>绑定的声明yaml文件的规范则如下:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">dapr.io/v1alpha1</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Component</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">name</span>: <span style="color:#ae81ff">&lt;NAME&gt;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">&lt;NAMESPACE&gt;</span> </span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>: </span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">bindings.&lt;TYPE&gt;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">version</span>: <span style="color:#ae81ff">v1</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">metadata</span>: </span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">&lt;NAME&gt;</span> </span></span><span style="display:flex;"><span> <span style="color:#f92672">value</span>: <span style="color:#ae81ff">&lt;VALUE&gt;</span> </span></span></code></pre></div><p>其中<code>metadata.name</code>则是绑定置名称,<code>spec.metadata.name</code>和<code>spec.metadata.value</code>则是配置的属性和对应值。这个值我们可以通过实现接口<code>InputBinding</code>或者<code>OutputBinding</code>实现输入绑定和输出绑定.</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">InputBinding</span> <span style="color:#66d9ef">interface</span> { </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Init</span>(<span style="color:#a6e22e">metadata</span> <span style="color:#a6e22e">Metadata</span>) <span style="color:#66d9ef">error</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Read</span>(<span style="color:#a6e22e">handler</span> <span style="color:#66d9ef">func</span>(<span style="color:#f92672">*</span><span style="color:#a6e22e">ReadResponse</span>) ([]<span style="color:#66d9ef">byte</span>, <span style="color:#66d9ef">error</span>)) <span style="color:#66d9ef">error</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">OutputBinding</span> <span style="color:#66d9ef">interface</span> { </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Init</span>(<span style="color:#a6e22e">metadata</span> <span style="color:#a6e22e">Metadata</span>) <span style="color:#66d9ef">error</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Invoke</span>(<span style="color:#a6e22e">req</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">InvokeRequest</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">InvokeResponse</span>, <span style="color:#66d9ef">error</span>) </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Operations</span>() []<span style="color:#a6e22e">OperationKind</span> </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><p>接下来需要实现一个生成对象的方法,比如说我们需要实现一个飞书推送Webhook的绑定组件,则可以:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">FeishuWebhook</span> <span style="color:#66d9ef">struct</span> { </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">logger</span> <span style="color:#a6e22e">logger</span>.<span style="color:#a6e22e">Logger</span> <span style="color:#75715e">// 这个是dapr的日志接口,输出日志可以使用这个</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">settings</span> <span style="color:#a6e22e">Settings</span> <span style="color:#75715e">// 具体配置信息</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">httpClient</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Client</span> <span style="color:#75715e">// 请求HTTP</span> </span></span><span style="display:flex;"><span>} </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewFeishuWebhook</span>(<span style="color:#a6e22e">l</span> <span style="color:#a6e22e">logger</span>.<span style="color:#a6e22e">Logger</span>) <span style="color:#f92672">*</span><span style="color:#a6e22e">FeishuWebhook</span> { </span></span><span style="display:flex;"><span> <span style="color:#75715e">// See guidance on proper HTTP client settings here:</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">// https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">dialer</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">Dialer</span>{ <span style="color:#75715e">//nolint:exhaustivestruct</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Timeout</span>: <span style="color:#ae81ff">5</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>, </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">netTransport</span> = <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Transport</span>{ <span style="color:#75715e">//nolint:exhaustivestruct</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">DialContext</span>: <span style="color:#a6e22e">dialer</span>.<span style="color:#a6e22e">DialContext</span>, </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">TLSHandshakeTimeout</span>: <span style="color:#ae81ff">5</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>, </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">httpClient</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Client</span>{ <span style="color:#75715e">//nolint:exhaustivestruct</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Timeout</span>: <span style="color:#a6e22e">defaultHTTPClientTimeout</span>, </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Transport</span>: <span style="color:#a6e22e">netTransport</span>, </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">FeishuWebhook</span>{ <span style="color:#75715e">//nolint:exhaustivestruct</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">logger</span>: <span style="color:#a6e22e">l</span>, </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">httpClient</span>: <span style="color:#a6e22e">httpClient</span>, </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><p>在绑定组件生命周期中,init会在初始化是进行调用,传入我们之前在yaml文件中定义的配置文件,因此我们可以在这里实现具体的配置获取:</p>构建属于你自己的dapr服务发现https://www.4async.com/2021/05/building-your-own-dapr-service-discovery/Sat, 08 May 2021 18:38:00 +0000https://www.4async.com/2021/05/building-your-own-dapr-service-discovery/<img src="https://vip1.loli.io/2022/03/26/uJdVWNeXC6Z3Hq8.png" alt="Featured image of post 构建属于你自己的dapr服务发现" /><p><strong>写在最前: 这篇文章其实算是马后炮了,因为一直拖延症的问题,顺带过了一个五一假期,结果发现已经有社区贡献者提供了Consul的服务发现实现,于是本来写了一半的文章只能进行调整了。拖延症害人啊!几个草稿的文章看来要尽快赶出来了🤦‍♂️</strong></p> <p>在<a class="link" href="./2021/03/2021-03-11-running-dapr-without-container/" >上一篇文章</a>中,我其实遗留了一个问题:如何定义dapr的服务发现呢?其实在后面阅读dapr的源码之后也前一篇文章的评论中提到了答案:目前dapr提供了内置两种服务发现模式:K8s模式和用于独立部署的mDNS模式。mDNS模式在某些网络环境下可能存在问题(比如跨机房),不过没有关系,dapr同时提供了可扩展能力,可以通过定义自主的服务发现能力扩展dapr的边界。</p> <h2 id="从-nameresolution-到-resolver-接口">从 NameResolution 到 Resolver 接口 </h2><p>在 <a class="link" href="https://github.com/dapr/dapr/blob/9341e3faa67ede470febe987b3398775759cf38f/pkg/components/nameresolution/registry.go#L25" target="_blank" rel="noopener" ><code>pkg/components/nameresolution/registry.go</code></a> 文件中,dapr定义了一个 <code>NameResolution</code> 结构体用于服务注册和发现:</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> ( </span></span><span style="display:flex;"><span> <span style="color:#75715e">// NameResolution is a name resolution component definition.</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">NameResolution</span> <span style="color:#66d9ef">struct</span> { </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Name</span> <span style="color:#66d9ef">string</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">FactoryMethod</span> <span style="color:#66d9ef">func</span>() <span style="color:#a6e22e">nr</span>.<span style="color:#a6e22e">Resolver</span> </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">// Registry handles registering and creating name resolution components.</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Registry</span> <span style="color:#66d9ef">interface</span> { </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Register</span>(<span style="color:#a6e22e">components</span> <span style="color:#f92672">...</span><span style="color:#a6e22e">NameResolution</span>) </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Create</span>(<span style="color:#a6e22e">name</span>, <span style="color:#a6e22e">version</span> <span style="color:#66d9ef">string</span>) (<span style="color:#a6e22e">nr</span>.<span style="color:#a6e22e">Resolver</span>, <span style="color:#66d9ef">error</span>) </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">nameResolutionRegistry</span> <span style="color:#66d9ef">struct</span> { </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">resolvers</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">func</span>() <span style="color:#a6e22e">nr</span>.<span style="color:#a6e22e">Resolver</span> </span></span><span style="display:flex;"><span> } </span></span><span style="display:flex;"><span>) </span></span></code></pre></div><p>其中真正的服务解析则是依靠 <a class="link" href="https://github.com/dapr/components-contrib/blob/3ef025c604952ad78101ecb52afe1c7c92c7d455/nameresolution/nameresolution.go#L9" target="_blank" rel="noopener" ><code>components-contrib</code></a> 中实现了 <code>Resolver</code> 接口的具体实现执行。</p> <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// Resolver is the interface of name resolver.</span> </span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Resolver</span> <span style="color:#66d9ef">interface</span> { </span></span><span style="display:flex;"><span> <span style="color:#75715e">// Init initializes name resolver.</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">Init</span>(<span style="color:#a6e22e">metadata</span> <span style="color:#a6e22e">Metadata</span>) <span style="color:#66d9ef">error</span> </span></span><span style="display:flex;"><span> <span style="color:#75715e">// ResolveID resolves name to address.</span> </span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ResolveID</span>(<span style="color:#a6e22e">req</span> <span style="color:#a6e22e">ResolveRequest</span>) (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">error</span>) </span></span><span style="display:flex;"><span>} </span></span></code></pre></div><p>其中 <code>Init</code> 会在 Runtime 初始化时被调用,而 <code>ResolveID</code> 则会在服务查询时调用。比如在 <a class="link" href="https://github.com/dapr/dapr/blob/1cbf67f8a5da0bb5be4e3a123632c17ebb173f4e/pkg/messaging/direct_messaging.go#L219" target="_blank" rel="noopener" ><code>pkg/messaging/direct_messaging.go</code></a> 的方法 <code>getRemoteApp</code> 中进行服务的解析:</p>