# Building an Alien Language from Scratch with LangChain

> Source: <https://dev.to/harishkotra/building-an-alien-language-from-scratch-with-langchain-43ji>
> Published: 2026-05-29 16:07:37+00:00

**How 100 AI agents evolved a shared symbolic vocabulary through trade — with no pre-programmed definitions, no mock data, and no shortcuts.**

Most emergent communication demos cheat. They hardcode symbol meanings, or use mock LLM responses, or simulate the entire thing with random number generators. The result is a demo that looks impressive but teaches you nothing about how actual language models behave when they need to coordinate.

I wanted to build something real: two civilizations of AI agents, each with 50 unique personalities, negotiating trades using abstract symbols — where **every single message and decision flows through a real LLM via LangChain SDK pipelines**.

No mock data. No hardcoded symbol mappings. Just pure reinforcement: successful trades reinforce a symbol's meaning, failed trades weaken it. Over hundreds of rounds, a shared vocabulary emerges.

LangChain.js (v1.4) is the backbone of this project. I deliberately used three different LangChain patterns to show how the SDK can handle different levels of agent complexity:

``` js
const chain = ChatPromptTemplate
  .fromMessages([["system", prompt], ["human", "{input}"]])
  .pipe(model)
  .pipe(parser);
```

Every symbol an agent sends is generated through this pipeline. The `ChatPromptTemplate`

injects the agent's personality, observations, recent successes/failures, and conversation history. The model returns structured JSON (`{"message": "🟢⚡🔺"}`

), which the `JsonOutputParser`

validates.

The trickiest part was the `{{`

escaping — LangChain uses `{}`

for template variables, but I needed literal `{}`

in the JSON example. The double-brace escape (`{{"message": "..."}}`

) resolves this cleanly.

Same structure, different prompt — but this one returns a boolean decision. `{"accept": true}`

or `{"accept": false}`

. The agent evaluates whether the proposed resource exchange is beneficial based on its observations and personality.

This is where LangChain really shines. Using `createAgent()`

from the LangGraph-based SDK, I built a full ReAct agent that can:

`DynamicTool`

`DynamicTool`

The agent literally thinks, uses tools, and then decides — all within LangChain's agent loop.

The moment you switch from mock to real LLMs, everything gets interesting. Models wrap JSON in markdown code fences. They include chain-of-thought reasoning. They run out of tokens mid-response. They return empty `content`

and put their thoughts in vendor-specific fields like `reasoning_content`

.

I built a `LenientJsonParser`

that extends LangChain's `JsonOutputParser`

to handle all of these cases:

```
class LenientJsonParser extends JsonOutputParser {
  async parse(text: string): Promise<object> {
    try { return await super.parse(text); } catch {
      // Strip ```
{% endraw %}
json ...
{% raw %}
 ``` fences
      // Match balanced braces for nested objects
      // Fall back gracefully
    }
  }
}
```

This was essential. Without it, models like Gemma 4 (a reasoning model) would output all their thinking in `reasoning_content`

and leave `content`

empty, causing the parser to fail on every message.

All three provider types — OpenAI, Featherless.ai, and LM Studio — use the same `ChatOpenAI`

class from `@langchain/openai`

. This means:

`baseURL`

config changeThe model factory in `orchestrator.ts`

creates the right `ChatOpenAI`

instance based on the config:

```
function createChatModel(config: SimulationConfig) {
  switch (config.provider) {
    case "openai":    return new ChatOpenAI({ model: "gpt-4o-mini", ... });
    case "featherless": return new ChatOpenAI({ model: "...", configuration: { baseURL: "..." } });
    case "lmstudio":  return new ChatOpenAI({ model: "...", configuration: { baseURL: ".../v1" } });
  }
}
```

Every provider swap is instant — no restart, no rebuild.

I went through two complete redesigns. The first was dark cyberpunk — glowing elements, neon accents, dramatic. It looked great in screenshots but was exhausting to work with.

The second is a premium light product aesthetic inspired by Civilization VI, Notion, and Stripe:

`box-shadow: 0 1px 2px rgba(0,0,0,0.04)`

)Dark/light mode uses CSS custom properties mapped through `@theme`

, with `localStorage`

persistence and `prefers-color-scheme`

detection. No Tailwind `dark:`

variants cluttering the component code.

**Real LLMs are messy** — `JsonOutputParser`

fails the moment a model wraps output in markdown. Build lenient parsers from day one.

**Reasoning models need special handling** — Gemma 4, DeepSeek R1, and similar models put everything in `reasoning_content`

and leave `content`

empty. You need higher `maxTokens`

and explicit "do not reason" instructions in the system prompt.

** {{ escaping is essential** — LangChain's

`ChatPromptTemplate`

uses `{}`

for template variables. JSON examples need `{{`

and `}}`

to produce literal braces in the output.** createAgent() is powerful but expensive** — the ReAct loop makes multiple LLM calls per decision. For simple trade evaluation, Pattern 2 (a single chain call) is more efficient. Use Pattern 3 when you need tool-use and multi-step reasoning.

**Template variable collision is real** — LangChain's `{}`

syntax conflicts with JSON's `{}`

. The switch to `{{`

escaping was a "wait, that's it?" moment after hours of debugging.

**CSS custom properties + Tailwind = clean themes** — mapping all colors through `@theme`

variables instead of using `dark:`

variants keeps component code readable and theming centralized.

```
git clone https://github.com/harishkotra/alien-translator.git
cd alien-translator
npm install
npm run dev
```

Open the settings, configure an LLM provider, and click Start. Watch the language emerge.

Code and more: [https://www.dailybuild.xyz/project/147-alient-translator](https://www.dailybuild.xyz/project/147-alient-translator)
