AI 资讯
How I Cut My Multimodal AI Costs by 97% — A Freelancer's Guide
How I Cut My Multimodal AI Costs by 97% — A Freelancer's Guide Last month I almost killed a side gig because of a single line item on an invoice. A client wanted me to build a document-processing tool that could read scanned PDFs, pull text out of photos, and answer questions about charts. Easy enough — except I'd quoted the job assuming I'd use GPT-4o for the vision work. When I actually ran the numbers, I realized the API bill would eat my entire margin. I'd be working for free. Maybe worse. So I did what every freelancer does when the big-name vendor gets too expensive: I went hunting. And I landed on Global API, which routes to a bunch of multimodal models I've honestly never heard clients talk about. After a few weeks of testing, I figured out which ones are worth my billable hours and which ones aren't. This is everything I learned, plus the exact code I'm shipping to clients. Why Multimodal Even Matters for Solo Devs Two years ago, "multimodal" was a buzzword you'd hear at conferences. In 2026 it's table stakes. I've personally used vision models to: OCR receipts for an expense-tracking app (boring but pays the rent) Convert screenshots of legacy code into editable source for a Y2K-era company migration Read bar charts from PDF reports for a finance client who hates spreadsheets Analyze medical imaging samples for a startup MVP (this one was scary) Every one of those jobs started as a quick conversation with a prospect and turned into real invoices because I could say yes. The bottleneck was never capability — it was always cost. When GPT-4o charges north of $10/M output tokens, a single 2,000-token response on a tricky chart costs me about two cents. Multiply by 10,000 images per month and you've got a $200 API line item before you've paid yourself. That's a problem when the whole job is worth $400. So I tested every multimodal model I could find on Global API. Here's the lineup I ended up evaluating. The Contenders Nine models, three providers, one freelanc
AI 资讯
I let Claude Code run --dangerously-skip-permissions on my production DB. Here's what I changed.
Last Tuesday at 3am, a multi-agent loop hit 12K KV writes/minute and froze. The loop was a one-line counter bug. That part was fixable. What I found while tracing it was worse. I had --dangerously-skip-permissions enabled on a Claude Code session that was running D1 migrations. I thought it was pointing at staging. It wasn't — I'd misconfigured my env file reference, loading .env.production instead of .dev.vars . Claude didn't ask. The flag told it not to. The migration was ADD COLUMN , not DROP COLUMN , so no data loss. Survivable. But only barely. The thing I got wrong: I treated --dangerously-skip-permissions as "skip the annoying confirmation popups." It's actually "remove the only moment a human sees what command is about to run." Those are very different things. Turning the flag back off helps, but it doesn't constrain what Claude attempts — it just adds a prompt you'll click through anyway at 3am. What actually worked was adding a deny rule in .claude/settings.json : { "permissions" : { "allow" : [ "Bash(wrangler d1 execute * --local*)" ], "deny" : [ "Bash(wrangler d1 execute *)" ] } } The allow rule is more specific than the deny, so --local calls go through and everything else is blocked before execution. Over 2 weeks post-fix, Claude attempted zero production DB commands. Three deny events were logged — all from ambiguous prompts I wrote during fast context-switches, not from Claude going rogue. I ended up running three layers: the settings.json allowlist, a separate git worktree for migration work that physically contains only staging credentials, and a CLAUDE.md that instructs Claude to ask before anything touching production. The CLAUDE.md approach has a real caveat though — in long sessions the instructions lose weight as context grows. Anything critical needs to be restated in the prompt itself. I wrote up the full breakdown — including the worktree setup, the exact CLAUDE.md wording, and why MCP tool permissions behave inconsistently with the deny ru
AI 资讯
What is Generative AI? Understanding the Foundation of Modern AI Agents #2
Everyone is talking about AI Agents. But before you build an AI Agent, there is one concept you absolutely need to understand: Generative AI. Generative AI is the technology that transformed software from systems that simply follow rules into systems that can understand language, generate responses, reason through instructions, and assist users in a natural way. As part of my new course: Develop Your First AI Agent with Microsoft Foundry I published the first lesson where we explore the journey from traditional software to Generative AI and understand why modern AI Agents became possible. 🎥 Watch the video here: Why This Topic Matters Many developers jump directly into AI Agents, prompts, tools, and frameworks. However, without understanding the evolution of AI, it becomes difficult to understand: Why AI Agents exist Why Large Language Models are important Why prompts work Why tools are needed How modern AI systems actually operate In this lesson, we start from first principles and build the foundation required for the rest of the course. What You'll Learn Traditional Software For decades, software followed a simple pattern: Input → Rules → Output Developers explicitly defined every behavior. This worked well until humans started interacting with software using natural language. Why Rule-Based Systems Break Imagine building a dietician chatbot. Users might ask: What should I eat? Suggest a healthy breakfast. What foods contain protein? Can I eat oats daily? All of these questions are similar. Yet they are phrased differently. Supporting thousands of variations quickly becomes impossible with manually written rules. Predictive AI Machine Learning introduced a new approach. Instead of writing rules, we train models using data. Examples include: Spam Detection Fraud Detection Recommendation Systems Predictive AI can make decisions. But it still cannot create content. Prediction vs Creation A predictive model can answer: Fraud probability: 87% But can it explain why? Ca
AI 资讯
The hard part of national ID OCR isn't the OCR
You wire up OCR for your KYC flow, point it at a national ID card, and get back a clean { name, idNumber, dateOfBirth } . Ship it. Then you onboard your second country — and it falls apart. Fields you mapped don't exist. The name comes back as garbled Latin. The date of birth says the year 2567. Here's the thing nobody tells you when you start: the hard part of national ID OCR isn't the OCR. It's that every country's ID is a different document. A model that reads text off a card is table stakes. Turning 30 countries' cards into data your system can actually use is where the work is. Let me show you the three axes of variation that will bite you, then how to architect so they don't. Axis 1: the fields are different There is no universal "national ID" schema, because the cards themselves don't agree on what to print. A Thai ID card prints the holder's religion . A German ID card prints height and eye color . A Chinese ID card prints ethnicity and the issuing authority. None of these are edge cases — they're core fields on those documents. So the instinct to define one IdCard type with a fixed set of columns is wrong from day one. Either you drop information that some countries consider essential, or you end up with a sparse table full of null s and country-specific special-casing. And it's not just which fields exist — it's what they're called and how they're split. The same "name" concept might come back as a single full-name string on one card and as separate given/family fields on another, sometimes in two scripts at once. Your data model has to treat "the field set depends on the country" as a first-class fact, not an afterthought. Axis 2: the script is different If your users are global, a lot of their names are not in the Latin alphabet — Chinese, Thai, Arabic, and more. The naive move is to transliterate everything to Latin "so it's consistent." Don't. Transliteration is lossy and ambiguous: multiple native spellings collapse to the same Latin form, diacritics
AI 资讯
Structuring TypeScript: Interfaces, Type Aliases, Enums, and Object Types
Structuring TypeScript: Interfaces, Type Aliases, Enums, and Object Types You've learned TypeScript's primitive types and the basics of type inference here . Now it's time to model real-world data — users, orders, API responses, configuration objects. That's where interfaces, type aliases, and enums come in. These three features are what make TypeScript genuinely powerful for building applications. Let's dig in. Object Types: Describing the Shape of Data Before we get to interfaces, let's understand object types. When you want to describe the structure of an object, you define what properties it has and what types those properties are: // Inline object type annotation function displayUser ( user : { name : string ; age : number ; email : string }): void { console . log ( ` ${ user . name } ( ${ user . age } ) — ${ user . email } ` ); } This works, but it's messy to repeat everywhere. That's why we use type aliases and interfaces to name and reuse these shapes. Type Aliases: Naming a Type A type alias gives a name to any type — primitives, unions, objects, or combinations: // Alias for a primitive union type ID = string | number ; // Alias for an object shape type User = { id : ID ; name : string ; age : number ; email : string ; }; // Now use it anywhere const user : User = { id : 1 , name : " Ramesh " , age : 31 , email : " ramesh@example.com " , }; function getUser ( id : ID ): User { // ... fetch user logic } Type aliases are flexible — they can represent almost anything. Interfaces: Defining Object Contracts An interface is specifically designed to describe the shape of an object. Syntax is slightly different: interface User { id : number ; name : string ; age : number ; email : string ; } const user : User = { id : 1 , name : " Ramesh " , age : 31 , email : " ramesh@example.com " , }; Optional and Readonly Properties Properties can be marked as optional ( ? ) or read-only ( readonly ): interface UserProfile { readonly id : number ; // Can't be changed after cre
AI 资讯
TypeScript Types Demystified: Simple Types, Special Types, and Type Inference
TypeScript Types Demystified: Simple Types, Special Types, and Type Inference In the first post , we covered why TypeScript exists and how to write your first program. Now it's time to get comfortable with the type system itself — the foundation everything else is built on. By the end of this post, you'll know how to type variables, arrays, and function parameters correctly. You'll also understand the "special" types that trip up most beginners: any , unknown , never , and void . The Core Primitive Types TypeScript's basic types map directly to JavaScript's primitives: // string let firstName : string = " Ramesh " ; let greeting : string = `Hello, ${ firstName } ` ; // number (no separate int/float — it's all number) let age : number = 31 ; let price : number = 9.99 ; let hex : number = 0xFF ; // boolean let isLoggedIn : boolean = true ; let hasAccess : boolean = false ; These are the types you'll use most often. Simple, predictable, and exactly what you'd expect. Type Inference: TypeScript Does the Work You don't always have to write the type. TypeScript infers it from the value you assign: let city = " Chennai " ; // TypeScript infers: string let year = 2026 ; // TypeScript infers: number let isActive = true ; // TypeScript infers: boolean Once inferred, that type is locked in: let city = " Chennai " ; city = 42 ; // ❌ Error: Type 'number' is not assignable to type 'string' Rule of thumb: Let TypeScript infer types for local variables. Write explicit annotations for function parameters and return types. // Let inference work for variables const scores = [ 95 , 87 , 72 ]; // inferred as number[] // Be explicit for function signatures function calculateAverage ( scores : number []): number { return scores . reduce (( a , b ) => a + b , 0 ) / scores . length ; } Explicit vs Inferred — When to Choose Each // ✅ Explicit annotation — good for function params & return types function formatName ( first : string , last : string ): string { return ` ${ first } ${ last } ` ;
AI 资讯
TypeScript Explained: Why Every JavaScript Developer Should Care
TypeScript Explained: Why Every JavaScript Developer Should Care You've been writing JavaScript for years. It works. So why bother with TypeScript? That's what I thought too — until I spent two days debugging a production bug that turned out to be a simple typo in a property name. A bug TypeScript would have caught in milliseconds. In this post, I'll explain what TypeScript is, why it exists, and how to write your very first TypeScript program. No fluff — just what you actually need to know. What Is TypeScript? TypeScript is JavaScript with types added on top. That's really it. It was created by Microsoft in 2012 and has since become one of the most popular tools in the JavaScript ecosystem — used by teams at Google, Airbnb, Slack, and countless others. Here's the key thing to understand: TypeScript is not a replacement for JavaScript . It compiles down to plain JavaScript. Every browser, Node.js server, and JavaScript runtime runs the same JS it always has. TypeScript just helps you write better code before that happens. JavaScript vs TypeScript — A Side-by-Side Look Let's say you're writing a function to greet a user: JavaScript: function greetUser ( name ) { return " Hello, " + name . toUpperCase (); } greetUser ( 42 ); // Runtime error: name.toUpperCase is not a function You won't discover this mistake until the code runs — possibly in production, in front of real users. TypeScript: function greetUser ( name : string ): string { return " Hello, " + name . toUpperCase (); } greetUser ( 42 ); // ❌ Error: Argument of type 'number' is not assignable to parameter of type 'string' TypeScript catches this immediately in your editor — before you even run the code. That : string annotation tells TypeScript exactly what type name should be. Why Use TypeScript? The Real Benefits 1. Catch Bugs Early The most obvious benefit. Instead of runtime errors that crash your app, TypeScript surfaces type errors at compile time — while you're still writing code. 2. Better Autocomplet
AI 资讯
Context Architecture: the day I realized the whole repo is the context
Your repo is already your agents' context, whether you designed it on purpose or not That sentence took me a while to understand. In this post I'll save you the trip. It was October 2025, working in Skyward's monorepo with AI agents every day. And every day the same routine: I'd tell the agent in the prompt "don't use this", "don't do it this way", "reuse the component that already exists". I wrote it down. I repeated it. The agent did exactly what I told it not to do. It wasn't that it didn't listen to me. It was that it read the code and saw something else there. The agent believes the code, not your prompt An agent follows the patterns it sees in the repo, not the ones you tell it in the prompt. And subagents are worse, because they start without the conversation's context. The whole fight you put up earlier in the chat, for them it never happened. So this is what kept happening. It created a new component even though one already existed that solved exactly that problem. It didn't respect the design rules or use the design tokens. It followed stale docs because they were still there, alive, with nothing flagging them as outdated. My first instinct was everyone's instinct, cram more context into the prompt. More rules, more "please don't do this", more examples pasted in by hand. It half worked, and for the next task you had to add it all again. Until the next subagent showed up and started from scratch. At some point, tired of repeating myself, I understood the obvious thing. The agent wasn't disobeying me. It was reading the repo and listening to what the repo said about itself. If the good component lives alongside three old versions, it has no way to know which one is the official one. If the docs say one thing and the code does another, it'll believe whichever is closest at hand. It's doing exactly what I asked. The repo itself is the context agents use. If it's badly structured, the answers won't be good. Period. No prompt fixes a repo that contradicts itsel
开发者
Context Architecture: el día que entendí que el repo entero es el contexto
Tu repo ya es el contexto de tus agentes, lo hayas diseñado a propósito o no Esa frase me costó entenderla. En este post te ahorro el camino. Era Octubre del 2025, trabajando en el monorepo de Skyward con agentes de IA todos los días. Y todos los días la misma rutina: le decía en el prompt al agente "no uses esto", "no hagas esto así", "reutiliza el componente que ya existe". Lo escribía. Lo repetía. El agente hacía exactamente lo que le dije que no hiciera. No era que no me escuchara. Era que leía el código y ahí veía otra cosa. El agente le cree al código, no a tu prompt Un agente sigue los patrones que ve en el repo, no los que tú le dices en el prompt. Y los subagentes son peores, porque arrancan sin el contexto de la conversación. Toda la pelea que tú diste arriba en el chat, para ellos no existió nunca. Entonces pasaba esto. Creaba un componente nuevo aunque ya había uno que resolvía exactamente el problema. No respetaba las normas de diseño ni usaba los design tokens. Seguía documentación obsoleta porque seguía ahí, viva, sin nada que la marcara como obsoleta. Mi primer instinto fue el de todos, meter más contexto en el prompt. Más reglas, más "por favor no hagas esto", más ejemplos pegados a mano. Funcionaba a medias, y para la siguiente tarea había que volver a agregarlo todo. Hasta que llegaba el siguiente subagente y empezaba de cero. En algún momento, cansado de repetirme, entendí lo obvio. El agente no me estaba desobedeciendo. Estaba leyendo el repo y haciendo caso a lo que el repo decía de sí mismo. Si conviven el componente bueno y tres versiones viejas, no tiene cómo saber cuál es el oficial. Si la doc dice una cosa y el código hace otra, le va a creer al que esté más a mano. Está haciendo justo lo que le pedí. El mismo repo es el contexto que usan los agentes. Si está mal estructurado, las respuestas no van a ser buenas. Punto. No hay prompt que arregle un repo que se contradice a sí mismo. Screaming Architecture me llevó hasta media cancha Lo prim
AI 资讯
How to Automate DNC Removal Requests in Convoso
DNC removal requests shouldn't take more than a few seconds to process. If your ops team is manually logging into each system, finding the number, and removing it one platform at a time, every request is an open compliance window. Here's how to close it automatically. The Problem With Manual DNC Processing A number comes in flagged for removal. Someone on the floor submits it. If you're running Convoso alongside Zoom Contact Center, Zoom Phone, and Telesero, that means logging into each system separately — find the number, remove it, move to the next platform, repeat. At multiple removal requests per week across several systems, you're looking at significant manual work each week. More importantly, every minute between the request and the removal is a minute of active compliance exposure. A TCPA violation starts at $500 per call. When the pattern is systematic — a number that should have been removed staying active across multiple campaigns — class action exposure enters the picture. The gap between when a removal is requested and when it actually completes isn't just inefficiency. It's risk that compounds with every dial attempt on a number that should be off the list. How Automated DNC Removal Works The automated version uses a Slack slash command as the intake point. An ops manager types the number into a command and hits send. The request routes immediately to a cloud service — deployed on Google Cloud Run — that fans out across every active system in parallel. Not sequentially. Simultaneously. In a contact center running multiple Convoso campaigns alongside Zoom Contact Center, Zoom Phone, and Telesero, a single command hits every platform in parallel. Each system processes the removal independently. Results log to cloud storage with a timestamp and each system's individual response recorded separately. A confirmation returns to the Slack channel before the manager has switched back to their next task. Wall-clock time from submission to confirmed removal across
AI 资讯
As China looms, Taiwan makes more drones for defense and the US military
Taiwan's drone spending plans for defense could also boost business overseas.
AI 资讯
The White House Is Making Up Its Rules for AI in Real Time
Anthropic still can’t distribute Claude Mythos or Fable 5 after running afoul of the Trump administration. But no one can say exactly what the company did wrong.
AI 资讯
Rivian owners file lawsuit alleging false promises on self-driving features
Plaintiffs in the class action complaint allege Rivian falsely promised for years it would bring hands-free driving to its first-generation R1 vehicles.
AI 资讯
Meta’s AI Workers Are Revolting, Peter Thiel’s Secret Society, and SBF’s Plea to Trump
On today’s Uncanny Valley, we dive into the dysfunction in Meta’s newly formed AI unit and why it’s been driving already-low employee morale even further into the ground.
AI 资讯
Building a Kubernetes Cluster on Red Hat Enterprise Linux 10: A kubeadm Guide
Introduction In this post, I'll walk you through deploying a production-ready Kubernetes cluster on Red Hat Enterprise Linux 10 using kubeadm. This lab was inspired by Anthony E. Nocentino's excellent Certified Kubernetes Administrator (CKA): Using kubadm to Install a Basic Cluster training course, which is part of the official Certified Kubernetes Administrator (CKA) path on Pluralsight . ⭐ Shout-out: Anthony is a fantastic trainer! His course uses Ubuntu 22.04 as the base OS. I adapted his approach to work on RHEL 10, adding some additional considerations specific to Red Hat's ecosystem. One intentional decision in this setup: I deployed Kubernetes v1.35 and CRI-O v1.35, which wasn't the latest version available at installation time. This was purposeful. Anthony's course includes a dedicated section on upgrading clusters, and using a slightly older baseline makes that learning path clearer. The upgrade procedures (not covered here) are what really solidify your understanding of cluster lifecycle management. Lab Infrastructure Overview Nodes Configuration Node Role RAM vCPUs IP Address rh-cp1 Control Plane 12 GiB 2 192.168.110.120 rh-node1 Worker 6 GiB 2 192.168.110.121 rh-node2 Worker 6 GiB 2 192.168.110.122 rh-node3 Worker 6 GiB 2 192.168.110.123 Note: The IP address schema is just an example and what was more convenient for me. Supporting Infrastructure A dedicated utilities VM (also RHEL 10) provides essential services: DNS (BIND/named) NTP (chrony) HTTP (Apache/httpd) DHCP (Kea) This centralized infrastructure simplifies name resolution across all cluster nodes. But this is not essential for this project. You can, instead, ensure the nodes are able to reach each other updating the file /etc/hosts on all nodes. Prerequisites & OS Preparation Before diving into Kubernetes, we need consistent node preparation across all machines . 1. System Registration and Updates $ sudo subscription-manager register --username <username> --password <password> $ sudo dnf update
AI 资讯
Rivian owners sue over false promises on self-driving features
Plaintiffs in the class action complaint allege Rivian falsely promised for years it would bring hands-free driving to its first-generation R1 vehicles.
AI 资讯
3 Amazon Workers Say They’re Under Investigation for Speaking Out About Data Centers
The software engineers filed a complaint with Seattle’s civil rights office accusing Amazon of illegally retaliating against them for expressing their personal political beliefs.
AI 资讯
Agent Framework RAG for Agents: Giving Your Agent the Right Context
This is Part 13 of my series on the Microsoft Agent Framework. You can read the original post over on lukaswalter.dev . In the previous article , we looked at workflows. Workflows make sense when the process itself needs structure: state, checkpoints, events, human approvals, and resumable execution. This post is the bridge from Agent Framework into RAG. I plan on doing a full RAG deep dive sometime later. The practical question for now is smaller: How do I connect an Agent Framework agent to private application knowledge without stuffing every document into the prompt? For agents, RAG is less about adding more text and more about giving the agent a controlled retrieval path. The agent should fetch the right context at the point where it needs it. Agents do not know your private data Your company documents, product catalog, tickets, rules, policies, runbooks, and internal knowledge base live outside the model. The model has generic knowledge. Your application has private knowledge. Treat those as separate systems. You can paste some private data into the prompt, and for a demo that may be enough. But this falls apart quickly: full documents are expensive to send repeatedly long prompts are fragile stale documents may sit next to current ones users may not be allowed to see every source long context still needs selection The last point is easy to underestimate. A larger context window lets you send more text. It does not decide which text is correct, current, relevant, or permitted. Do not give the agent all knowledge. Give it the right context at the moment it needs it. Retrieval owns that job. The minimal RAG shape The basic RAG loop is small: user question -> retrieve relevant chunks -> pass chunks to the agent -> agent answers using that context For documents, the longer pipeline usually looks like this: documents -> chunks -> embeddings -> vector store -> search -> retrieved context -> agent response Documents are split into smaller chunks. Those chunks are embe
AI 资讯
Preparing Specs for AI Coding Agents
AI coding agents now edit repositories, run commands, and produce branches. That makes the spec before the work more important: it carries the context, boundaries, and success criteria the agent needs. What a good coding-agent spec includes Specs are becoming more important because AI coding agents are no longer only answering questions. They are reading repositories, editing files, running commands, producing branches, and asking humans to review the result. That changes what a prompt needs to become. When an assistant only answers a question, a private prompt can be enough. When an agent changes a shared codebase, the prompt becomes an assignment. And an assignment needs more than good wording. It needs the right context, boundaries, examples, and a way to judge whether the work matched the original intent. That is the practical reason to prepare a spec before sending a coding agent into a repository. The spec does not need to be long. It does need to tell the agent what problem it is solving, what behavior should change, what must not change, and how the result will be reviewed. At minimum, a good coding-agent spec should give the agent five things: the context behind the task the behavior that should change the constraints the agent should preserve examples or scenarios that define correctness the validation evidence a reviewer should inspect This is the useful idea behind spec-driven development, behavior scenarios, issue templates, lightweight design docs, OpenSpec, GitHub Spec Kit, and many internal engineering proposal formats. The specific framework matters less than the shape of the spec: the agent should receive enough context to act, and the team should receive enough structure to review the result. The spec is not a nicer prompt. It is the prepared assignment between human intent and machine execution. Prompts are good at starting work. Specs are better at carrying it. A private prompt is optimized for immediacy. It lives in a chat session. It can inclu
AI 资讯
How to Integrate Apache Kafka with Spring Boot: A Production-Ready Guide
When a Spring Boot service needs to talk to another service without waiting on a synchronous HTTP call, message queues are the usual answer. Apache Kafka has become the default choice for this in most backend teams, but a lot of tutorials stop at a "hello world" producer and consumer that would never survive a real production load. Things like consumer retries, error handling, serialization of real objects, and graceful shutdown get skipped, and those are exactly the parts that page you at 2 a.m. In this tutorial, you will build a Spring Boot application that produces and consumes JSON messages over Kafka. You will configure a producer and a consumer, send a typed object instead of a plain string, handle deserialization errors so one bad message does not block your whole consumer group, and verify the whole thing works end to end. By the end, you will have a small but realistic messaging setup you can build on. Prerequisites To follow along, you will need: Java 17 or later installed. You can check your version by running java -version . A Spring Boot 3.x project. You can generate one at start.spring.io with the Spring for Apache Kafka dependency added. A running Kafka broker. The quickest way to get one locally is Docker, which the first step covers. Basic familiarity with Spring Boot, including how @Component and application.yml work. Step 1 — Running Kafka Locally with Docker Before writing any code, you need a broker to talk to. Running Kafka by hand involves Zookeeper, broker configuration, and a fair amount of setup, so you will use Docker Compose to bring up a single-broker cluster instead. Create a file named docker-compose.yml in your project root: services : kafka : image : apache/kafka:3.7.0 container_name : kafka ports : - " 9092:9092" environment : KAFKA_NODE_ID : 1 KAFKA_PROCESS_ROLES : broker,controller KAFKA_LISTENERS : PLAINTEXT://:9092,CONTROLLER://:9093 KAFKA_ADVERTISED_LISTENERS : PLAINTEXT://localhost:9092 KAFKA_CONTROLLER_LISTENER_NAMES : CONTRO